From 13c94c69ce661348ced1d677919d48b5406b58f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Hlad=C3=ADk?= Date: Mon, 9 Dec 2024 15:38:04 +0100 Subject: [PATCH 1/5] Remove deprecated DSL api --- .../app/futured/kmptemplate/gradle/ext/KotlinTargetExt.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2f6c0851..d300cff6 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(), From 07dcf742532bb263d3c44a48f6fa8c967f32892c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Hlad=C3=ADk?= Date: Thu, 6 Feb 2025 11:58:08 +0100 Subject: [PATCH 2/5] Add ComponentFactory for ScreenComponent generator with KSP and KotlinPoet --- .../component-factory-annotations.gradle.kts | 29 ++++ gradle/libs.versions.toml | 6 + init_template.kts | 2 + settings.gradle.kts | 2 + .../annotation/build.gradle.kts | 22 +++ .../annotation/GenerateFactory.kt | 5 + .../processor/build.gradle.kts | 33 ++++ .../processor/ComponentFactoryProcessor.kt | 29 ++++ .../ComponentFactoryProcessorProvider.kt | 10 ++ .../PoetFactoryComponentGenerator.kt | 145 ++++++++++++++++++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + shared/feature/build.gradle.kts | 3 + .../navigation/home/HomeNavHostComponent.kt | 34 +--- .../profile/ProfileNavHostComponent.kt | 24 +-- .../navigation/root/RootNavHostComponent.kt | 12 +- .../feature/ui/firstScreen/FirstComponent.kt | 2 + .../feature/ui/loginScreen/LoginComponent.kt | 2 + .../ui/profileScreen/ProfileComponent.kt | 2 + .../ui/secondScreen/SecondComponent.kt | 2 + .../feature/ui/thirdScreen/ThirdComponent.kt | 9 +- 20 files changed, 314 insertions(+), 60 deletions(-) create mode 100644 convention-plugins/src/main/kotlin/component-factory-annotations.gradle.kts create mode 100644 shared/factory-generator/annotation/build.gradle.kts create mode 100644 shared/factory-generator/annotation/src/commonMain/kotlin/app/futured/factorygenerator/annotation/GenerateFactory.kt create mode 100644 shared/factory-generator/processor/build.gradle.kts create mode 100644 shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt create mode 100644 shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessorProvider.kt create mode 100644 shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt create mode 100644 shared/factory-generator/processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/convention-plugins/src/main/kotlin/component-factory-annotations.gradle.kts b/convention-plugins/src/main/kotlin/component-factory-annotations.gradle.kts new file mode 100644 index 00000000..31155913 --- /dev/null +++ b/convention-plugins/src/main/kotlin/component-factory-annotations.gradle.kts @@ -0,0 +1,29 @@ +import gradle.kotlin.dsl.accessors._411868000f8e772c6f299e8a28e6e73d.ksp +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.kotlin.dsl.the + +// As of now, we cannot use Gradle version catalogs with Precompiled Script plugins: https://github.com/gradle/gradle/issues/15383 +plugins { + id("com.google.devtools.ksp") +} + +// https://github.com/gradle/gradle/issues/15383 +val libs = the() +// Enable source generation by KSP to commonMain only +dependencies { + add("kspCommonMainMetadata", project(":shared:factory-generator:processor")) + // DO NOT add bellow dependencies +// add("kspAndroid", Deps.Koin.kspCompiler) +// add("kspIosX64", Deps.Koin.kspCompiler) +// add("kspIosArm64", Deps.Koin.kspCompiler) +// add("kspIosSimulatorArm64", Deps.Koin.kspCompiler) +} + +// WORKAROUND: ADD this dependsOn("kspCommonMainKotlinMetadata") instead of above dependencies +tasks.withType>().configureEach { + if (name != "kspCommonMainKotlinMetadata") { + dependsOn("kspCommonMainKotlinMetadata") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6951f9c0..3025e4b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ profileinstaller = "1.3.1" dokkaVersion = "1.9.20" google-servicesPlugin = "4.4.2" google-firebaseAppDistributionPlugin = "5.0.0" +poet = "2.0.0" # Android Namespaces project-android-namespace = "app.futured.kmptemplate.android" @@ -92,6 +93,10 @@ decompose = { group = "com.arkivanov.decompose", name = "decompose", version.ref decompose-compose-ext = { group = "com.arkivanov.decompose", name = "extensions-compose", version.ref = "decompose" } essenty = { group = "com.arkivanov.essenty", name = "lifecycle", version.ref = "essenty" } +# 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"} + # Testing kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } @@ -164,6 +169,7 @@ firebase-distribution = { id = "com.google.firebase.appdistribution", version.re # Precompiled script plugins conventions-lint = { id = "conventions-lint" } koin-annotations-plugin = { id = "koin-annotations" } +component-factory-annotations-plugin = { id = "component-factory-annotations" } [bundles] compose = [ diff --git a/init_template.kts b/init_template.kts index 92da0f85..2925f93e 100755 --- a/init_template.kts +++ b/init_template.kts @@ -172,6 +172,8 @@ fun renamePackagesInShared(packageName: String) { "platform", "resources", "util", + "factory-generator/annotation", + "factory-generator/processor", ) modules.forEach { moduleName -> sourceSets.forEach { targetName -> diff --git a/settings.gradle.kts b/settings.gradle.kts index 0684f126..77a0246d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,8 @@ include(":shared:feature") include(":shared:persistence") include(":shared:platform") include(":shared:resources") +include(":shared:factory-generator:annotation") +include(":shared:factory-generator:processor") includeBuild("convention-plugins") include(":baselineprofile") diff --git a/shared/factory-generator/annotation/build.gradle.kts b/shared/factory-generator/annotation/build.gradle.kts new file mode 100644 index 00000000..e135b3ff --- /dev/null +++ b/shared/factory-generator/annotation/build.gradle.kts @@ -0,0 +1,22 @@ +import app.futured.kmptemplate.gradle.configuration.ProjectSettings +import app.futured.kmptemplate.gradle.ext.iosTargets + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) +} + +kotlin { + jvmToolchain(ProjectSettings.Kotlin.JvmToolchainVersion) + + applyDefaultHierarchyTemplate() + + jvm() + + iosTargets() + + sourceSets { + commonMain { + dependencies { } + } + } +} diff --git a/shared/factory-generator/annotation/src/commonMain/kotlin/app/futured/factorygenerator/annotation/GenerateFactory.kt b/shared/factory-generator/annotation/src/commonMain/kotlin/app/futured/factorygenerator/annotation/GenerateFactory.kt new file mode 100644 index 00000000..05112744 --- /dev/null +++ b/shared/factory-generator/annotation/src/commonMain/kotlin/app/futured/factorygenerator/annotation/GenerateFactory.kt @@ -0,0 +1,5 @@ +package app.futured.factorygenerator.annotation + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class GenerateFactory diff --git a/shared/factory-generator/processor/build.gradle.kts b/shared/factory-generator/processor/build.gradle.kts new file mode 100644 index 00000000..8c62a5c7 --- /dev/null +++ b/shared/factory-generator/processor/build.gradle.kts @@ -0,0 +1,33 @@ +import app.futured.kmptemplate.gradle.configuration.ProjectSettings +import app.futured.kmptemplate.gradle.ext.iosTargets + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) +} + +kotlin { + jvmToolchain(ProjectSettings.Kotlin.JvmToolchainVersion) + + applyDefaultHierarchyTemplate() + + jvm() + + iosTargets() + + sourceSets { + commonMain { + dependencies { + implementation(projects.shared.factoryGenerator.annotation) + } + } + + jvmMain { + dependencies { + implementation(libs.ksp.lib) + implementation(libs.poet.interop) + } + kotlin.srcDir("src/main/kotlin") + resources.srcDir("src/main/resources") + } + } +} diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt new file mode 100644 index 00000000..a142d3bb --- /dev/null +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt @@ -0,0 +1,29 @@ +package app.futured.factorygenerator.processor + +import app.futured.factorygenerator.annotation.GenerateFactory +import com.google.devtools.ksp.processing.CodeGenerator +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 kotlin.reflect.KClass + +class ComponentFactoryProcessor( + private val codeGenerator: CodeGenerator, +) : SymbolProcessor { + override fun process(resolver: Resolver): List { + val components: Sequence = resolver.findAnnotationsForClass(GenerateFactory::class) + + components.forEach { generateComponent(it) } + + return emptyList() + } + + private fun generateComponent(component: KSClassDeclaration) = + PoetFactoryComponentGenerator.generateFactory(component, codeGenerator) + + private fun Resolver.findAnnotationsForClass(kClass: KClass<*>): Sequence = + this.getSymbolsWithAnnotation(kClass.qualifiedName.toString()) + .filterIsInstance() +} + diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessorProvider.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessorProvider.kt new file mode 100644 index 00000000..be5dab53 --- /dev/null +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessorProvider.kt @@ -0,0 +1,10 @@ +package app.futured.factorygenerator.processor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class ComponentFactoryProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = + ComponentFactoryProcessor(codeGenerator = environment.codeGenerator) +} diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt new file mode 100644 index 00000000..09055fa2 --- /dev/null +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt @@ -0,0 +1,145 @@ +package app.futured.factorygenerator.processor + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSValueParameter +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo + +/** + * `PoetFactoryComponentGenerator` is responsible for generating the factory object for a given component. + * + * This object utilizes KotlinPoet to generate the necessary code for creating a factory that + * uses Koin for dependency injection. It analyzes a component's constructor parameters to + * determine which dependencies need to be manually injected and generates a `createComponent` function + * that retrieves these dependencies from the Koin container. + * + * The generated factory object implements the `KoinComponent` interface and provides a + * `createComponent` function to instantiate the target component. + * + * Key functionalities: + * 1. **Generates Factory Objects:** Creates a factory object for each component annotated + * with a specific annotation (implicitly handled by the ksp processor). + * 2. **Handles Dependency Injection:** Uses Koin's `get()` function to retrieve necessary + * dependencies from the Koin container. + * 3. **Supports Constructor Parameters:** Inspects the component's constructor to identify + * parameters requiring manual injection (marked with `@InjectedParam`). + * 4. **Handles Special Types:** Detects and handles special types like `AppComponentContext`, + * `Navigation`, and `Arg` for specific component needs. + */ +object PoetFactoryComponentGenerator { + + private const val INJECTED_PARAM_ANNOTATION = "InjectedParam" + private const val APP_COMPONENT_CONTEXT_TYPE_NAME = "AppComponentContext" + private const val ARGUMENT_TYPE_NAME = "Arg" + private const val NAVIGATION_TYPE_NAME = "Navigation" + + private object Imports { + const val KOIN_COMPONENT_PACKAGE = "org.koin.core.component" + const val KOIN_PARAMETER_PACKAGE = "org.koin.core.parameter" + const val KOIN_COMPONENT_CLASS_NAME = "KoinComponent" + const val KOIN_GET_FUNCTION_NAME = "get" + const val KOIN_PARAMETERS_OF_FUNCTION_NAME = "parametersOf" + } + + fun generateFactory( + factoryComponent: KSClassDeclaration, + codeGenerator: CodeGenerator, + ) { + // Component name without package e.g. FirstComponent + val baseName: String = factoryComponent.qualifiedName?.asString()?.substringAfterLast('.') ?: error("Unable to get base name") + + val factoryComponentPackageName = factoryComponent.packageName.asString() + val factoryClassName = ClassName( + packageName = factoryComponentPackageName, + simpleNames = listOf("${baseName}Factory"), + ) + + val koinComponentClass = ClassName( + packageName = "org.koin.core.component", + simpleNames = listOf("KoinComponent"), + ) + + val componentTypeSpec = createComponentTypeSpec( + factoryClassName = factoryClassName, + koinComponentClass = koinComponentClass, + createComponentFunction = createComponentFunction(baseName, factoryComponentPackageName, factoryComponent), + ) + + val fileSpec = createFileSpec(factoryClassName, componentTypeSpec) + + fileSpec.writeTo(codeGenerator, aggregating = true) + } + + private fun createComponentTypeSpec( + factoryClassName: ClassName, + koinComponentClass: ClassName, + createComponentFunction: FunSpec, + ) = TypeSpec.objectBuilder(factoryClassName) + .addModifiers(KModifier.INTERNAL) + .addSuperinterface(superinterface = koinComponentClass) + .addFunction(createComponentFunction) + .build() + + private fun createFileSpec(factoryClassName: ClassName, componentTypeSpec: TypeSpec) = FileSpec.builder(factoryClassName) + .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_COMPONENT_CLASS_NAME) + .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_GET_FUNCTION_NAME) + .addImport(Imports.KOIN_PARAMETER_PACKAGE, Imports.KOIN_PARAMETERS_OF_FUNCTION_NAME) + .addType(componentTypeSpec) + .build() + + private fun createComponentFunction( + baseName: String, + factoryComponentPackageName: String, + factoryComponent: KSClassDeclaration, + ): FunSpec { + // All constructor parameters that are annotated with @InjectedParam + val unInjectedConstructorParams = factoryComponent.primaryConstructor?.parameters + ?.filter { it.annotations.any { it.shortName.asString() == INJECTED_PARAM_ANNOTATION } } + + val appComponentContextType = unInjectedConstructorParams + ?.findTypeByName(APP_COMPONENT_CONTEXT_TYPE_NAME) + ?: error("Unable to find $APP_COMPONENT_CONTEXT_TYPE_NAME in $baseName's constructor") + val navigationType = unInjectedConstructorParams + .findTypeByName(NAVIGATION_TYPE_NAME) ?: error("Unable to find Navigation type in $baseName's constructor") + val argsType = unInjectedConstructorParams.findTypeByName(ARGUMENT_TYPE_NAME) + + val returnType = ClassName( + packageName = factoryComponentPackageName, + simpleNames = listOf(baseName), + ) + + val params = if (argsType != null) { + "parameters = { parametersOf(componentContext, navigation, args) }" + } else { + "parameters = { parametersOf(componentContext, navigation) }" + } + val createComponentFunSpec = FunSpec.builder("createComponent") + .addParameter(name = "componentContext", appComponentContextType) + .addParameter(name = "navigation", navigationType) + + if (argsType != null) { + createComponentFunSpec.addParameter(name = "args", argsType) + } + + return createComponentFunSpec + .returns(returnType) + .addStatement( + "return get(\n" + + "qualifier = null,\n" + + "$params,\n" + + ")", + ) + .build() + } + + private fun List.findTypeByName(name: String): TypeName? = this + .find { it.type.toTypeName().toString().contains(name) } + ?.type?.toTypeName() +} diff --git a/shared/factory-generator/processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/shared/factory-generator/processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000..434e6ef4 --- /dev/null +++ b/shared/factory-generator/processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +app.futured.factorygenerator.processor.ComponentFactoryProcessorProvider \ No newline at end of file diff --git a/shared/feature/build.gradle.kts b/shared/feature/build.gradle.kts index b96d9f26..e03e4e50 100644 --- a/shared/feature/build.gradle.kts +++ b/shared/feature/build.gradle.kts @@ -8,6 +8,7 @@ plugins { id(libs.plugins.kotlin.parcelize.get().pluginId) id(libs.plugins.conventions.lint.get().pluginId) id(libs.plugins.koin.annotations.plugin.get().pluginId) + id(libs.plugins.component.factory.annotations.plugin.get().pluginId) alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.serialization) @@ -47,6 +48,8 @@ kotlin { implementation(projects.shared.persistence) implementation(projects.shared.arkitektDecompose) implementation(projects.shared.resources) + implementation(projects.shared.factoryGenerator.annotation) + implementation(libs.logging.kermit) implementation(libs.skie.annotations) implementation(libs.network.ktor.http) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt index e8f780dc..2f9783f7 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt @@ -3,13 +3,9 @@ package app.futured.kmptemplate.feature.navigation.home import app.futured.arkitekt.decompose.ext.asStateFlow import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext -import app.futured.kmptemplate.feature.ui.base.AppComponentFactory -import app.futured.kmptemplate.feature.ui.firstScreen.FirstComponent -import app.futured.kmptemplate.feature.ui.firstScreen.FirstScreenNavigation -import app.futured.kmptemplate.feature.ui.secondScreen.SecondComponent -import app.futured.kmptemplate.feature.ui.secondScreen.SecondScreenNavigation -import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent -import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation +import app.futured.kmptemplate.feature.ui.firstScreen.FirstComponentFactory +import app.futured.kmptemplate.feature.ui.secondScreen.SecondComponentFactory +import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponentFactory import com.arkivanov.decompose.Child import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack @@ -42,27 +38,9 @@ internal class HomeNavHostComponent( handleBackButton = true, childFactory = { config, childCtx -> when (config) { - HomeConfig.First -> HomeChild.First( - AppComponentFactory.createComponent( - childContext = childCtx, - navigation = homeNavigator, - ), - ) - - HomeConfig.Second -> HomeChild.Second( - AppComponentFactory.createComponent( - childContext = childCtx, - navigation = homeNavigator, - ), - ) - - is HomeConfig.Third -> HomeChild.Third( - AppComponentFactory.createComponent( - childContext = childCtx, - navigation = homeNavigator, - config.args, - ), - ) + HomeConfig.First -> HomeChild.First(FirstComponentFactory.createComponent(childCtx, homeNavigator)) + HomeConfig.Second -> HomeChild.Second(SecondComponentFactory.createComponent(childCtx, homeNavigator)) + is HomeConfig.Third -> HomeChild.Third(ThirdComponentFactory.createComponent(childCtx, homeNavigator, config.args)) } }, ).asStateFlow(componentCoroutineScope) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt index a4b881cf..c135e298 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt @@ -3,14 +3,10 @@ package app.futured.kmptemplate.feature.navigation.profile import app.futured.arkitekt.decompose.ext.asStateFlow import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext -import app.futured.kmptemplate.feature.ui.base.AppComponentFactory -import app.futured.kmptemplate.feature.ui.profileScreen.ProfileComponent -import app.futured.kmptemplate.feature.ui.profileScreen.ProfileScreenNavigation -import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent -import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation +import app.futured.kmptemplate.feature.ui.profileScreen.ProfileComponentFactory +import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponentFactory import com.arkivanov.decompose.Child import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.navigate import com.arkivanov.decompose.router.stack.pop @@ -34,20 +30,8 @@ internal class ProfileNavHostComponent( handleBackButton = true, childFactory = { config, childCtx -> when (config) { - ProfileConfig.Profile -> ProfileChild.Profile( - AppComponentFactory.createComponent( - childContext = childCtx, - navigation = navigator, - ), - ) - - is ProfileConfig.Third -> ProfileChild.Third( - AppComponentFactory.createComponent( - childContext = childCtx, - navigation = navigator, - config.args - ) - ) + ProfileConfig.Profile -> ProfileChild.Profile(ProfileComponentFactory.createComponent(childCtx, navigator)) + is ProfileConfig.Third -> ProfileChild.Third(ThirdComponentFactory.createComponent(childCtx, navigator, config.args)) } }, ).asStateFlow(componentCoroutineScope) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt index 7385fbea..c0b2e8a2 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt @@ -8,7 +8,7 @@ import app.futured.kmptemplate.feature.navigation.signedIn.SignedInNavHostNaviga import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.AppComponentFactory -import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponent +import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponentFactory import app.futured.kmptemplate.feature.ui.loginScreen.LoginScreenNavigation import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs import co.touchlab.kermit.Logger @@ -40,13 +40,9 @@ internal class RootNavHostComponent( childFactory = { config, childCtx -> when (config) { RootConfig.Login -> RootChild.Login( - screen = AppComponentFactory.createComponent( - childContext = childCtx, - navigation = LoginScreenNavigation( - toSignedIn = { - slotNavigator.activate(RootConfig.SignedIn()) - }, - ), + LoginComponentFactory.createComponent( + childCtx, + LoginScreenNavigation(toSignedIn = { slotNavigator.activate(RootConfig.SignedIn()) }), ), ) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt index c6c74549..ef0d5c8f 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt @@ -1,5 +1,6 @@ package app.futured.kmptemplate.feature.ui.firstScreen +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.domain.CounterUseCase import app.futured.kmptemplate.feature.domain.CounterUseCaseArgs import app.futured.kmptemplate.feature.domain.SyncDataUseCase @@ -16,6 +17,7 @@ import org.koin.core.annotation.InjectedParam import kotlin.time.Duration.Companion.milliseconds @Factory +@GenerateFactory internal class FirstComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam override val navigation: FirstScreenNavigation, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt index 2f19ea11..41f70eef 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt @@ -1,11 +1,13 @@ package app.futured.kmptemplate.feature.ui.loginScreen +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.ScreenComponent import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class LoginComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt index b15055f7..eee62742 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt @@ -1,11 +1,13 @@ package app.futured.kmptemplate.feature.ui.profileScreen +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.ScreenComponent import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class ProfileComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt index fc96c224..b9531d18 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt @@ -1,6 +1,7 @@ package app.futured.kmptemplate.feature.ui.secondScreen import app.futured.arkitekt.decompose.ext.asStateFlow +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.AppComponentFactory import app.futured.kmptemplate.feature.ui.base.ScreenComponent @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class SecondComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt index 03b0239e..c664ce74 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt @@ -1,5 +1,6 @@ package app.futured.kmptemplate.feature.ui.thirdScreen +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.ScreenComponent import app.futured.kmptemplate.resources.MR @@ -8,16 +9,16 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class ThirdComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam args: ThirdScreenArgs, @InjectedParam override val navigation: ThirdScreenNavigation, ) : ScreenComponent( - componentContext = componentContext, - defaultState = ThirdViewState(text = MR.strings.third_screen_text.format(args.id)), - ), - ThirdScreen, ThirdScreenNavigation by navigation { + componentContext = componentContext, + defaultState = ThirdViewState(text = MR.strings.third_screen_text.format(args.id)), +), ThirdScreen, ThirdScreenNavigation by navigation { override val viewState: StateFlow = componentState From f67df9ab7e3f6b9ce1b68369523cd5f855391e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Hlad=C3=ADk?= Date: Fri, 7 Feb 2025 12:11:03 +0100 Subject: [PATCH 3/5] Fix generated param names and types --- .../factorygenerator/processor/ComponentFactoryProcessor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt index a142d3bb..9dbf14f0 100644 --- a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt @@ -2,10 +2,12 @@ package app.futured.factorygenerator.processor import app.futured.factorygenerator.annotation.GenerateFactory 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 kotlin.math.log import kotlin.reflect.KClass class ComponentFactoryProcessor( @@ -19,7 +21,7 @@ class ComponentFactoryProcessor( return emptyList() } - private fun generateComponent(component: KSClassDeclaration) = + private fun generateComponent(component: KSClassDeclaration, logger: KSPLogger) = PoetFactoryComponentGenerator.generateFactory(component, codeGenerator) private fun Resolver.findAnnotationsForClass(kClass: KClass<*>): Sequence = From aa97ca165b94858db3e7499da01c72f68d48f10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Hlad=C3=ADk?= Date: Fri, 7 Feb 2025 12:11:38 +0100 Subject: [PATCH 4/5] Use Generated factory methods --- .../processor/ComponentFactoryProcessor.kt | 4 +- .../PoetFactoryComponentGenerator.kt | 74 +++++++++++++------ .../navigation/home/HomeNavHostComponent.kt | 2 + .../profile/ProfileNavHostComponent.kt | 2 + .../navigation/root/RootNavHostComponent.kt | 16 ++-- .../signedIn/SignedInNavHostComponent.kt | 21 +++--- .../feature/ui/picker/FruitPickerComponent.kt | 2 + .../ui/picker/VegetablePickerComponent.kt | 2 + .../ui/secondScreen/SecondComponent.kt | 24 +++--- 9 files changed, 89 insertions(+), 58 deletions(-) diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt index 9dbf14f0..a142d3bb 100644 --- a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/ComponentFactoryProcessor.kt @@ -2,12 +2,10 @@ package app.futured.factorygenerator.processor import app.futured.factorygenerator.annotation.GenerateFactory 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 kotlin.math.log import kotlin.reflect.KClass class ComponentFactoryProcessor( @@ -21,7 +19,7 @@ class ComponentFactoryProcessor( return emptyList() } - private fun generateComponent(component: KSClassDeclaration, logger: KSPLogger) = + private fun generateComponent(component: KSClassDeclaration) = PoetFactoryComponentGenerator.generateFactory(component, codeGenerator) private fun Resolver.findAnnotationsForClass(kClass: KClass<*>): Sequence = diff --git a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt index 09055fa2..253afd3a 100644 --- a/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt @@ -37,7 +37,6 @@ object PoetFactoryComponentGenerator { private const val INJECTED_PARAM_ANNOTATION = "InjectedParam" private const val APP_COMPONENT_CONTEXT_TYPE_NAME = "AppComponentContext" - private const val ARGUMENT_TYPE_NAME = "Arg" private const val NAVIGATION_TYPE_NAME = "Navigation" private object Imports { @@ -53,7 +52,8 @@ object PoetFactoryComponentGenerator { codeGenerator: CodeGenerator, ) { // Component name without package e.g. FirstComponent - val baseName: String = factoryComponent.qualifiedName?.asString()?.substringAfterLast('.') ?: error("Unable to get base name") + val baseName: String = factoryComponent.qualifiedName?.asString()?.substringAfterLast('.') + ?: error("Unable to get base name") val factoryComponentPackageName = factoryComponent.packageName.asString() val factoryClassName = ClassName( @@ -69,7 +69,11 @@ object PoetFactoryComponentGenerator { val componentTypeSpec = createComponentTypeSpec( factoryClassName = factoryClassName, koinComponentClass = koinComponentClass, - createComponentFunction = createComponentFunction(baseName, factoryComponentPackageName, factoryComponent), + createComponentFunction = createComponentFunction( + baseName, + factoryComponentPackageName, + factoryComponent + ), ) val fileSpec = createFileSpec(factoryClassName, componentTypeSpec) @@ -87,12 +91,13 @@ object PoetFactoryComponentGenerator { .addFunction(createComponentFunction) .build() - private fun createFileSpec(factoryClassName: ClassName, componentTypeSpec: TypeSpec) = FileSpec.builder(factoryClassName) - .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_COMPONENT_CLASS_NAME) - .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_GET_FUNCTION_NAME) - .addImport(Imports.KOIN_PARAMETER_PACKAGE, Imports.KOIN_PARAMETERS_OF_FUNCTION_NAME) - .addType(componentTypeSpec) - .build() + private fun createFileSpec(factoryClassName: ClassName, componentTypeSpec: TypeSpec) = + FileSpec.builder(factoryClassName) + .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_COMPONENT_CLASS_NAME) + .addImport(Imports.KOIN_COMPONENT_PACKAGE, Imports.KOIN_GET_FUNCTION_NAME) + .addImport(Imports.KOIN_PARAMETER_PACKAGE, Imports.KOIN_PARAMETERS_OF_FUNCTION_NAME) + .addType(componentTypeSpec) + .build() private fun createComponentFunction( baseName: String, @@ -107,39 +112,64 @@ object PoetFactoryComponentGenerator { ?.findTypeByName(APP_COMPONENT_CONTEXT_TYPE_NAME) ?: error("Unable to find $APP_COMPONENT_CONTEXT_TYPE_NAME in $baseName's constructor") val navigationType = unInjectedConstructorParams - .findTypeByName(NAVIGATION_TYPE_NAME) ?: error("Unable to find Navigation type in $baseName's constructor") - val argsType = unInjectedConstructorParams.findTypeByName(ARGUMENT_TYPE_NAME) + .findTypeByName(NAVIGATION_TYPE_NAME) + val argsTypes = unInjectedConstructorParams + .filter { + it.containsTypeName(NAVIGATION_TYPE_NAME).not() && it.containsTypeName( + APP_COMPONENT_CONTEXT_TYPE_NAME + ).not() + } + .map { it.type.toTypeName() } val returnType = ClassName( packageName = factoryComponentPackageName, simpleNames = listOf(baseName), ) + val paramNameAndType = argsTypes.mapIndexed { index, arg -> + val paramName = + unInjectedConstructorParams.find { it.type.toTypeName() == arg }?.name?.asString() + ?: "param$index" + paramName to arg + } + val paramNames = paramNameAndType.joinToString { it.first } - val params = if (argsType != null) { - "parameters = { parametersOf(componentContext, navigation, args) }" - } else { - "parameters = { parametersOf(componentContext, navigation) }" + val params = when { + argsTypes.isNotEmpty() && navigationType != null -> "parameters = { parametersOf(componentContext, navigation, $paramNames) }" + argsTypes.isNotEmpty() -> "parameters = { parametersOf(componentContext, $paramNames) }" + navigationType != null -> "parameters = { parametersOf(componentContext, navigation) }" + else -> "parameters = { parametersOf(componentContext) }" } + val createComponentFunSpec = FunSpec.builder("createComponent") .addParameter(name = "componentContext", appComponentContextType) - .addParameter(name = "navigation", navigationType) - if (argsType != null) { - createComponentFunSpec.addParameter(name = "args", argsType) + if (navigationType != null) { + createComponentFunSpec.addParameter(name = "navigation", navigationType) + } + + if (paramNameAndType.isNotEmpty()) { + paramNameAndType.forEach { (name, type) -> + createComponentFunSpec.addParameter( + name = name, type + ) + } } return createComponentFunSpec .returns(returnType) .addStatement( "return get(\n" + - "qualifier = null,\n" + - "$params,\n" + - ")", + "qualifier = null,\n" + + "$params,\n" + + ")", ) .build() } private fun List.findTypeByName(name: String): TypeName? = this - .find { it.type.toTypeName().toString().contains(name) } + .find { it.containsTypeName(name) } ?.type?.toTypeName() + + private fun KSValueParameter.containsTypeName(name: String): Boolean = + this.type.toTypeName().toString().contains(name) } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt index afa1530c..6d6b30ca 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/home/HomeNavHostComponent.kt @@ -1,6 +1,7 @@ package app.futured.kmptemplate.feature.navigation.home import app.futured.arkitekt.decompose.ext.asStateFlow +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.firstScreen.FirstComponentFactory @@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class HomeNavHostComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt index 4a429ccc..1181f41b 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/profile/ProfileNavHostComponent.kt @@ -1,6 +1,7 @@ package app.futured.kmptemplate.feature.navigation.profile import app.futured.arkitekt.decompose.ext.asStateFlow +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.profileScreen.ProfileComponentFactory @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class ProfileNavHostComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt index 30e72077..b94f5220 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/root/RootNavHostComponent.kt @@ -3,14 +3,10 @@ package app.futured.kmptemplate.feature.navigation.root import app.futured.arkitekt.decompose.ext.asStateFlow import app.futured.kmptemplate.feature.navigation.deepLink.DeepLinkDestination import app.futured.kmptemplate.feature.navigation.deepLink.DeepLinkResolver -import app.futured.kmptemplate.feature.navigation.signedIn.SignedInNavHostComponent +import app.futured.kmptemplate.feature.navigation.signedIn.SignedInNavHostComponentFactory import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext -import app.futured.kmptemplate.feature.ui.base.AppComponentFactory -import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponent -import app.futured.kmptemplate.feature.ui.loginScreen.LoginScreen import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponentFactory -import app.futured.kmptemplate.feature.ui.loginScreen.LoginScreenNavigation import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs import co.touchlab.kermit.Logger import com.arkivanov.decompose.router.slot.ChildSlot @@ -42,15 +38,15 @@ internal class RootNavHostComponent( RootConfig.Login -> RootChild.Login( LoginComponentFactory.createComponent( childCtx, - LoginScreenNavigation(toSignedIn = { slotNavigator.activate(RootConfig.SignedIn()) }), + rootNavigator, ), ) is RootConfig.SignedIn -> RootChild.SignedIn( - navHost = AppComponentFactory.createAppComponent( - childContext = childCtx, - { rootNavigator.slotNavigator.activate(RootConfig.Login) }, - config.initialConfig, + navHost = SignedInNavHostComponentFactory.createComponent( + componentContext = childCtx, + navigationToLogin = { rootNavigator.slotNavigator.activate(RootConfig.Login) }, + initialConfig = config.initialConfig, ), ) } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedIn/SignedInNavHostComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedIn/SignedInNavHostComponent.kt index e143695e..5548b75f 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedIn/SignedInNavHostComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedIn/SignedInNavHostComponent.kt @@ -2,11 +2,11 @@ package app.futured.kmptemplate.feature.navigation.signedIn import app.futured.arkitekt.decompose.ext.asStateFlow import app.futured.arkitekt.decompose.ext.switchTab -import app.futured.kmptemplate.feature.navigation.home.HomeNavHostComponent -import app.futured.kmptemplate.feature.navigation.profile.ProfileNavHostComponent +import app.futured.factorygenerator.annotation.GenerateFactory +import app.futured.kmptemplate.feature.navigation.home.HomeNavHostComponentFactory +import app.futured.kmptemplate.feature.navigation.profile.ProfileNavHostComponentFactory import app.futured.kmptemplate.feature.ui.base.AppComponent import app.futured.kmptemplate.feature.ui.base.AppComponentContext -import app.futured.kmptemplate.feature.ui.base.AppComponentFactory import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.stateIn import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam +@GenerateFactory @Factory internal class SignedInNavHostComponent( @InjectedParam componentContext: AppComponentContext, @@ -36,17 +37,17 @@ internal class SignedInNavHostComponent( childFactory = { config, childCtx -> when (config) { is SignedInConfig.Home -> SignedInChild.Home( - AppComponentFactory.createAppComponent( - childContext = childCtx, - config.initialStack, + HomeNavHostComponentFactory.createComponent( + componentContext = childCtx, + initialStack = config.initialStack, ), ) is SignedInConfig.Profile -> SignedInChild.Profile( - navHost = AppComponentFactory.createAppComponent( - childContext = childCtx, - navigationToLogin, - config.initialStack, + navHost = ProfileNavHostComponentFactory.createComponent( + componentContext = childCtx, + toLogin = navigationToLogin, + initialStack = config.initialStack, ), ) } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt index 87efd46d..a9e3e0fd 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt @@ -1,5 +1,6 @@ package app.futured.kmptemplate.feature.ui.picker +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.ScreenComponent import app.futured.kmptemplate.resources.MR @@ -13,6 +14,7 @@ import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam import kotlin.time.Duration.Companion.seconds +@GenerateFactory @Factory internal class FruitPickerComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt index b7cb4638..4f32d955 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt @@ -1,5 +1,6 @@ package app.futured.kmptemplate.feature.ui.picker +import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext import app.futured.kmptemplate.feature.ui.base.ScreenComponent import app.futured.kmptemplate.resources.MR @@ -13,6 +14,7 @@ import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam import kotlin.time.Duration.Companion.seconds +@GenerateFactory @Factory internal class VegetablePickerComponent( @InjectedParam componentContext: AppComponentContext, diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt index 6dc75294..9142c9c6 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt @@ -3,12 +3,11 @@ package app.futured.kmptemplate.feature.ui.secondScreen import app.futured.arkitekt.decompose.ext.asStateFlow import app.futured.factorygenerator.annotation.GenerateFactory import app.futured.kmptemplate.feature.ui.base.AppComponentContext -import app.futured.kmptemplate.feature.ui.base.AppComponentFactory import app.futured.kmptemplate.feature.ui.base.ScreenComponent -import app.futured.kmptemplate.feature.ui.picker.FruitPickerComponent +import app.futured.kmptemplate.feature.ui.picker.FruitPickerComponentFactory import app.futured.kmptemplate.feature.ui.picker.Picker import app.futured.kmptemplate.feature.ui.picker.PickerNavigation -import app.futured.kmptemplate.feature.ui.picker.VegetablePickerComponent +import app.futured.kmptemplate.feature.ui.picker.VegetablePickerComponentFactory import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -24,10 +23,9 @@ internal class SecondComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam override val navigation: SecondScreenNavigation, ) : ScreenComponent( - componentContext = componentContext, - defaultState = SecondViewState, - ), - SecondScreen, SecondScreenNavigation by navigation { + componentContext = componentContext, + defaultState = SecondViewState, +), SecondScreen, SecondScreenNavigation by navigation { override val viewState: StateFlow = componentState @@ -46,14 +44,14 @@ internal class SecondComponent( serializer = SecondScreen.PickerType.serializer(), childFactory = { type, childContext -> when (type) { - SecondScreen.PickerType.Fruit -> AppComponentFactory.createAppComponent( - childContext, - pickerNavigation, + SecondScreen.PickerType.Fruit -> FruitPickerComponentFactory.createComponent( + componentContext = childContext, + navigation = pickerNavigation, ) - SecondScreen.PickerType.Vegetable -> AppComponentFactory.createAppComponent( - childContext, - pickerNavigation, + SecondScreen.PickerType.Vegetable -> VegetablePickerComponentFactory.createComponent( + componentContext = childContext, + navigation = pickerNavigation, ) } }, From 3b0b1f97000c046ecd7a450798ba864fdf8e7f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Hlad=C3=ADk?= Date: Fri, 7 Feb 2025 12:22:39 +0100 Subject: [PATCH 5/5] Update actions --- .../feature/ui/_template/_TemplateScreen.kt | 16 ++++++++-------- .../feature/ui/firstScreen/FirstComponent.kt | 9 ++++----- .../feature/ui/loginScreen/LoginComponent.kt | 9 ++++----- .../feature/ui/picker/FruitPickerComponent.kt | 13 +++++++------ .../ui/picker/VegetablePickerComponent.kt | 12 ++++++------ .../feature/ui/profileScreen/ProfileComponent.kt | 10 ++++------ .../feature/ui/secondScreen/SecondComponent.kt | 16 +++++++++------- .../feature/ui/thirdScreen/ThirdComponent.kt | 8 ++++---- 8 files changed, 46 insertions(+), 47 deletions(-) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/_template/_TemplateScreen.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/_template/_TemplateScreen.kt index 564ae784..c7434c08 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/_template/_TemplateScreen.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/_template/_TemplateScreen.kt @@ -22,9 +22,9 @@ interface TEMPLATEScreen { } } -internal data class TEMPLATEScreenNavigation( - val pop: () -> Unit, -) : NavigationActions +internal interface TEMPLATEScreenNavigation : NavigationActions { + fun pop() +} data object TEMPLATEViewState @@ -32,11 +32,11 @@ data object TEMPLATEViewState internal class TEMPLATEComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam override val navigation: TEMPLATEScreenNavigation, -) : ScreenComponent(componentContext, TEMPLATEViewState), TEMPLATEScreen { - - override val actions: TEMPLATEScreen.Actions = object : TEMPLATEScreen.Actions { - override fun onBack() = navigation.pop() - } +) : ScreenComponent(componentContext, TEMPLATEViewState), TEMPLATEScreen, + TEMPLATEScreen.Actions { + override val actions: TEMPLATEScreen.Actions = this override val viewState: StateFlow = componentState.asStateFlow() + + override fun onBack() = navigation.pop() } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt index ef0d5c8f..a7caa489 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/firstScreen/FirstComponent.kt @@ -26,7 +26,7 @@ internal class FirstComponent( ) : ScreenComponent( componentContext = componentContext, defaultState = FirstViewState(), -), FirstScreen, FirstScreenNavigation by navigation { +), FirstScreen, FirstScreenNavigation by navigation, FirstScreen.Actions { companion object { private const val COUNTER_ALERT = 10L @@ -35,10 +35,7 @@ internal class FirstComponent( private val logger = Logger.withTag("FirstComponent") override val viewState: StateFlow = componentState.asStateFlow() - - override val actions: FirstScreen.Actions = object : FirstScreen.Actions { - override fun onNext() = navigateToSecond() - } + override val actions: FirstScreen.Actions = this init { doOnCreate { @@ -47,6 +44,8 @@ internal class FirstComponent( } } + override fun onNext() = navigateToSecond() + private fun syncData() = syncDataUseCase.execute { onError { error -> logger.e(error) { error.message.toString() } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt index 8a580989..e38dee4d 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/loginScreen/LoginComponent.kt @@ -15,11 +15,10 @@ internal class LoginComponent( ) : ScreenComponent( componentContext = componentContext, defaultState = LoginViewState, -), LoginScreen, LoginScreenNavigation by navigation { - - override val actions: LoginScreen.Actions = object : LoginScreen.Actions { - override fun onLoginClick() = toSignedIn() - } +), LoginScreen, LoginScreenNavigation by navigation, LoginScreen.Actions { + override val actions: LoginScreen.Actions = this override val viewState: StateFlow = componentState.asStateFlow() + + override fun onLoginClick() = toSignedIn() } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt index a9e3e0fd..4bfc7f4d 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/FruitPickerComponent.kt @@ -20,15 +20,12 @@ internal class FruitPickerComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam override val navigation: PickerNavigation, ) : ScreenComponent(componentContext, PickerState()), - Picker { - - override val actions: Picker.Actions = object : Picker.Actions { - override fun onPick(item: String) = navigation.dismiss(item) - override fun onDismiss() = navigation.dismiss(null) - } + Picker, Picker.Actions { override val viewState: StateFlow = componentState.asStateFlow() + override val actions: Picker.Actions = this + init { doOnCreate { launchWithHandler { @@ -47,4 +44,8 @@ internal class FruitPickerComponent( } } } + + override fun onPick(item: String) = navigation.dismiss(item) + + override fun onDismiss() = navigation.dismiss(null) } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt index 4f32d955..cfd547e4 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/picker/VegetablePickerComponent.kt @@ -20,13 +20,9 @@ internal class VegetablePickerComponent( @InjectedParam componentContext: AppComponentContext, @InjectedParam override val navigation: PickerNavigation, ) : ScreenComponent(componentContext, PickerState()), - Picker { - - override val actions: Picker.Actions = object : Picker.Actions { - override fun onPick(item: String) = navigation.dismiss(item) - override fun onDismiss() = navigation.dismiss(null) - } + Picker, Picker.Actions { + override val actions: Picker.Actions = this override val viewState: StateFlow = componentState.asStateFlow() init { @@ -48,4 +44,8 @@ internal class VegetablePickerComponent( } } } + + override fun onPick(item: String) = navigation.dismiss(item) + + override fun onDismiss() = navigation.dismiss(null) } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt index eee62742..a87a8fa3 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/profileScreen/ProfileComponent.kt @@ -15,12 +15,10 @@ internal class ProfileComponent( ) : ScreenComponent( componentContext, ProfileViewState, -), ProfileScreen, ProfileScreenNavigation by navigation { - - override val actions: ProfileScreen.Actions = object : ProfileScreen.Actions { - override fun onLogout() = toLogin() - override fun onThird() = navigateToThird("hello third from profile") - } +), ProfileScreen, ProfileScreenNavigation by navigation, ProfileScreen.Actions { + override val actions: ProfileScreen.Actions = this override val viewState: StateFlow = componentState + override fun onLogout() = toLogin() + override fun onThird() = navigateToThird("hello third from profile") } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt index 9142c9c6..a07ec8e6 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/secondScreen/SecondComponent.kt @@ -25,9 +25,10 @@ internal class SecondComponent( ) : ScreenComponent( componentContext = componentContext, defaultState = SecondViewState, -), SecondScreen, SecondScreenNavigation by navigation { +), SecondScreen, SecondScreenNavigation by navigation, SecondScreen.Actions { override val viewState: StateFlow = componentState + override val actions: SecondScreen.Actions = this private val pickerNavigator = SlotNavigation() private val pickerNavigation = PickerNavigation( @@ -57,10 +58,11 @@ internal class SecondComponent( }, ).asStateFlow() - override val actions: SecondScreen.Actions = object : SecondScreen.Actions { - override fun onBack() = pop() - override fun onPickVeggie() = pickerNavigator.activate(SecondScreen.PickerType.Vegetable) - override fun onPickFruit() = pickerNavigator.activate(SecondScreen.PickerType.Fruit) - override fun onPickerDismissed() = pickerNavigator.dismiss() - } + override fun onBack() = pop() + + override fun onPickVeggie() = pickerNavigator.activate(SecondScreen.PickerType.Vegetable) + + override fun onPickFruit() = pickerNavigator.activate(SecondScreen.PickerType.Fruit) + + override fun onPickerDismissed() = pickerNavigator.dismiss() } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt index c664ce74..5a498c50 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/thirdScreen/ThirdComponent.kt @@ -18,11 +18,11 @@ internal class ThirdComponent( ) : ScreenComponent( componentContext = componentContext, defaultState = ThirdViewState(text = MR.strings.third_screen_text.format(args.id)), -), ThirdScreen, ThirdScreenNavigation by navigation { +), ThirdScreen, ThirdScreenNavigation by navigation, ThirdScreen.Actions { override val viewState: StateFlow = componentState - override val actions: ThirdScreen.Actions = object : ThirdScreen.Actions { - override fun onBack() = pop() - } + override val actions: ThirdScreen.Actions = this + + override fun onBack() = pop() }