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(), 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 702d626d..e80605a2 100755 --- a/init_template.kts +++ b/init_template.kts @@ -32,6 +32,8 @@ renameGradleSubproject("shared/network/rest", appPackageName) renameGradleSubproject("shared/persistence", appPackageName) renameGradleSubproject("shared/platform", appPackageName) renameGradleSubproject("shared/resources", appPackageName) +//"factory-generator/annotation", TODO ? +//"factory-generator/processor", TODO ? findAndReplaceInFileTree( parent = Path.of("shared/arkitekt-decompose"), 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..253afd3a --- /dev/null +++ b/shared/factory-generator/processor/src/jvmMain/kotlin/app/futured/factorygenerator/processor/PoetFactoryComponentGenerator.kt @@ -0,0 +1,175 @@ +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 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) + 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 = 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) + + 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" + + ")", + ) + .build() + } + + private fun List.findTypeByName(name: String): TypeName? = this + .find { it.containsTypeName(name) } + ?.type?.toTypeName() + + private fun KSValueParameter.containsTypeName(name: String): Boolean = + this.type.toTypeName().toString().contains(name) +} 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 8193927f..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,15 +1,12 @@ 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.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 @@ -19,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, @@ -35,27 +33,9 @@ internal class HomeNavHostComponent( handleBackButton = true, childFactory = { config, childCtx -> when (config) { - HomeConfig.First -> HomeChild.First( - AppComponentFactory.createScreenComponent( - childContext = childCtx, - navigation = homeNavigator, - ), - ) - - HomeConfig.Second -> HomeChild.Second( - AppComponentFactory.createScreenComponent( - childContext = childCtx, - navigation = homeNavigator, - ), - ) - - is HomeConfig.Third -> HomeChild.Third( - AppComponentFactory.createScreenComponent( - 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() 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 14476ce2..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,13 +1,11 @@ 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.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.childStack @@ -17,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, @@ -33,20 +32,8 @@ internal class ProfileNavHostComponent( handleBackButton = true, childFactory = { config, childCtx -> when (config) { - ProfileConfig.Profile -> ProfileChild.Profile( - AppComponentFactory.createScreenComponent( - childContext = childCtx, - navigation = navigator, - ), - ) - - is ProfileConfig.Third -> ProfileChild.Third( - AppComponentFactory.createScreenComponent( - 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() 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 7da034d4..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,13 +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.LoginScreenNavigation +import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponentFactory import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs import co.touchlab.kermit.Logger import com.arkivanov.decompose.router.slot.ChildSlot @@ -38,19 +35,18 @@ internal class RootNavHostComponent( handleBackButton = false, childFactory = { config, childCtx -> when (config) { - RootConfig.Login -> { - val screen: LoginScreen = AppComponentFactory.createScreenComponent( - childContext = childCtx, - navigation = rootNavigator, - ) - RootChild.Login(screen = screen) - } + RootConfig.Login -> RootChild.Login( + LoginComponentFactory.createComponent( + childCtx, + 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/_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 c6c74549..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 @@ -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, @@ -24,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 @@ -33,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 { @@ -45,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 18e398a1..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 @@ -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, @@ -13,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 87efd46d..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 @@ -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,20 +14,18 @@ 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, @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 { @@ -45,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 b7cb4638..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 @@ -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,18 +14,15 @@ 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, @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 { @@ -46,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 b15055f7..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 @@ -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, @@ -13,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 14720c7e..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 @@ -1,13 +1,13 @@ 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 @@ -17,17 +17,18 @@ 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, @InjectedParam override val navigation: SecondScreenNavigation, ) : ScreenComponent( - componentContext = componentContext, - defaultState = SecondViewState, - ), - SecondScreen, SecondScreenNavigation by navigation { + componentContext = componentContext, + defaultState = SecondViewState, +), 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( @@ -44,23 +45,24 @@ 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, ) } }, ).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 03b0239e..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 @@ -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,20 +9,20 @@ 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, 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() }