From f1826d333f5247572180877859cca96c4d03f1bb Mon Sep 17 00:00:00 2001 From: Roman Levinzon <33395420+levinzonr@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:31:40 +0200 Subject: [PATCH] Feat/sealed interface actions (#27) * feat: add new type of the actions handlers for the template * feat: add AI based generation for the Sealed actions * feat: ad UI for the actions type selection --- .../plugin/core/TemplateGenerator.kt | 18 ++++--- .../ai/domain/models/FeatureBreakdown.kt | 47 +++++++++++++++++++ .../newfeature/domain/models/ActionsType.kt | 5 ++ .../domain/models/FeatureConfiguration.kt | 3 +- .../domain/models/FeatureProperties.kt | 3 ++ .../FeatureConfigurationRepository.kt | 7 ++- .../newfeature/ui/advanced/AdvancedDialog.kt | 31 ++++++++++++ .../ui/advanced/AdvancedViewModel.kt | 17 +++++++ .../internal/ComposeContract.kt.ft | 11 +++++ .../internal/ComposeCoordinator.kt.ft | 21 ++++++--- .../fileTemplates/internal/ComposeRoute.kt.ft | 20 +++++++- .../internal/ComposeScreen.kt.ft | 14 +++++- 12 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/ActionsType.kt diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/TemplateGenerator.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/TemplateGenerator.kt index fa50dd5..8c66e0a 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/TemplateGenerator.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/TemplateGenerator.kt @@ -16,20 +16,24 @@ class TemplateGenerator(private val project: Project) { directory: PsiDirectory, properties: MutableMap ) : PsiFile { + try { - val existing = directory.findFile("${fileName}.kt") - if (existing != null) return existing + val existing = directory.findFile("${fileName}.kt") + if (existing != null) return existing - val manager = FileTemplateManager.getInstance(project) - val template = manager.getInternalTemplate(templateName) - properties[PropertyKeys.PackageName] = requireNotNull(directory.getPackageName()) - return FileTemplateUtil.createFromTemplate( + val manager = FileTemplateManager.getInstance(project) + val template = manager.getInternalTemplate(templateName) + properties[PropertyKeys.PackageName] = requireNotNull(directory.getPackageName()) + return FileTemplateUtil.createFromTemplate( template, "${fileName}.kt", properties.toProperties(), directory - ) as PsiFile + ) as PsiFile + } catch (e: Exception) { + throw IllegalStateException("Failed to generate file $fileName", e) + } } private fun PsiDirectory.getPackageName(): String? { diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/FeatureBreakdown.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/FeatureBreakdown.kt index d3ec22c..35f425a 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/FeatureBreakdown.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/FeatureBreakdown.kt @@ -41,6 +41,53 @@ data class FeatureBreakdown( } + fun actionsHandlers(baseName: String): String { + return """ + ${ + actions.joinToString("\n") { + if (it.params.isEmpty()) { + "data object ${it.name.drop(2)} : ${baseName}Action" + } else { + "data class ${it.name.drop(2)}(${ + it.params.joinToString(",\n") { + "val ${it.name}: ${it.type}" + } + }) : ${baseName}Action" + } + } + } + """.trimIndent() + } + + fun coordinatorHandlers(baseName: String): String { + return """ + fun handle(action: ${baseName}Action) { + when(action) { + ${ + actions.joinToString("\n") { + val imperativeHandler = if (it.type != Action.Type.Other) { + "viewModel.${it.imperativeName}(${it.params.joinToString(","){ + "action.${it.name}" + }})" + } else { + "// handle ${it.imperativeName} action" + } + if (it.params.isEmpty()) { + + "${baseName}Action.${it.name.drop(2)} -> {\n $imperativeHandler \n}" + } else { + "is ${baseName}Action.${it.name.drop(2)} -> {\n $imperativeHandler \n}" + } + } + } + } + } + + + """.trimIndent() + } + + val viewModelActions = actions.filter { it.type != Action.Type.Other }.joinToString("\n") { val params = it.namedParams val param = it.params.firstOrNull()?.name diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/ActionsType.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/ActionsType.kt new file mode 100644 index 0000000..97c3b39 --- /dev/null +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/ActionsType.kt @@ -0,0 +1,5 @@ +package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models + +enum class ActionsType { + Data, Sealed +} \ No newline at end of file diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureConfiguration.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureConfiguration.kt index 31ec34f..0345337 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureConfiguration.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureConfiguration.kt @@ -7,5 +7,6 @@ data class FeatureConfiguration( val useCollectFlowWithLifecycle: Boolean, val usePreviewParameterProvider: Boolean, val injection: InjectionConfiguration, - val navigationType: NavigationType + val navigationType: NavigationType, + val actionsType: ActionsType ) \ No newline at end of file diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureProperties.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureProperties.kt index 6afeec0..42c84b6 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureProperties.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureProperties.kt @@ -27,6 +27,9 @@ data class FeatureProperties( PropertyKeys.NAVIGATION_CLASS_SUFFIX to "Destination", "VM_ACTIONS" to breakdown?.viewModelActions.orEmpty(), "NAV_TYPE" to config.navigationType.name, + "ACTIONS_TYPE" to config.actionsType.name, + "COORDINATOR_HANDLERS" to breakdown?.coordinatorHandlers(name).orEmpty(), + "SEALED_ACTIONS" to breakdown?.actionsHandlers(name).orEmpty() ) } } diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/repository/FeatureConfigurationRepository.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/repository/FeatureConfigurationRepository.kt index 1e52d41..4770c08 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/repository/FeatureConfigurationRepository.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/repository/FeatureConfigurationRepository.kt @@ -2,6 +2,7 @@ package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repo import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSource import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationType +import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.ActionsType import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.FeatureConfiguration import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.InjectionConfiguration @@ -15,17 +16,20 @@ class FeatureConfigurationRepository( put(KEY_FLOW_LIFECYCLE, features.useCollectFlowWithLifecycle) put(KEY_PREVIEW_PARAMETER_PROVIDER, features.usePreviewParameterProvider) put(KEY_NAVIGATION_TYPE, features.navigationType.name) + put(KEY_ACTIONS_TYPE, features.actionsType.name) } } fun get() : FeatureConfiguration { val injection = dataSource.get(KEY_INJECTION, InjectionConfiguration.Hilt.name) val navigationType = dataSource.get(KEY_NAVIGATION_TYPE, NavigationType.Kiwi.name) + val actionsType = dataSource.get(KEY_ACTIONS_TYPE, ActionsType.Data.name) return FeatureConfiguration( useCollectFlowWithLifecycle = dataSource.get(KEY_FLOW_LIFECYCLE, true), usePreviewParameterProvider = dataSource.get(KEY_PREVIEW_PARAMETER_PROVIDER, true), injection = InjectionConfiguration.valueOf(injection), - navigationType = NavigationType.valueOf(navigationType) + navigationType = NavigationType.valueOf(navigationType), + actionsType = ActionsType.valueOf(actionsType) ) } @@ -38,5 +42,6 @@ class FeatureConfigurationRepository( private const val KEY_PREVIEW_PARAMETER_PROVIDER = "use_preview_parameter_provider" private const val KEY_INJECTION = "view_model_injection" private const val KEY_NAVIGATION_TYPE = "navigation_type" + private const val KEY_ACTIONS_TYPE = "actions_type" } } \ No newline at end of file diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedDialog.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedDialog.kt index 8b303b0..151c51a 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedDialog.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedDialog.kt @@ -17,6 +17,12 @@ class AdvancedDialog( override fun createPanel(): DialogPanel { return panel { + + row { + rowComment("Configure advanced settings for the feature" + + "settings will be persisted for the current project.") + } + group("State Collection") { row { checkBox("Use collectAsStateWithLifecycle") @@ -59,6 +65,31 @@ class AdvancedDialog( } } + group("Actions Provider") { + buttonsGroup { + + row { + radioButton("Data Class") + .bindSelected(viewModel::dataClassActionsSetter) + } + + row { + comment("All actions will be generated as data classes (default)") + } + + row { + radioButton("Sealed Interface") + .bindSelected(viewModel::sealedActionsSetter) + } + + row { + comment("Actions will be generated as a sealed interface i.e LoginAction.UsernameChange") + } + + + } + } + group { row { text( diff --git a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedViewModel.kt b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedViewModel.kt index faf4795..8e08293 100644 --- a/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedViewModel.kt +++ b/src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedViewModel.kt @@ -1,6 +1,7 @@ package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationType +import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.ActionsType import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository.FeatureConfigurationRepository import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.InjectionConfiguration @@ -35,6 +36,12 @@ class AdvancedViewModel( featureConfigurationRepository.put(featureConfigurationRepository.get().copy(navigationType = value)) } + private var actionsType: ActionsType + get() = featureConfigurationRepository.get().actionsType + set(value) { + featureConfigurationRepository.put(featureConfigurationRepository.get().copy(actionsType = value)) + } + var kiwiSetter: Boolean get() = navigationType == NavigationType.Kiwi @@ -63,5 +70,15 @@ class AdvancedViewModel( } + var sealedActionsSetter: Boolean + get() = actionsType == ActionsType.Sealed + set(value) { + if (value) actionsType = ActionsType.Sealed + } + var dataClassActionsSetter: Boolean + get() = actionsType == ActionsType.Data + set(value) { + if (value) actionsType = ActionsType.Data + } } \ No newline at end of file diff --git a/src/main/resources/fileTemplates/internal/ComposeContract.kt.ft b/src/main/resources/fileTemplates/internal/ComposeContract.kt.ft index 0351176..5ccd5a4 100644 --- a/src/main/resources/fileTemplates/internal/ComposeContract.kt.ft +++ b/src/main/resources/fileTemplates/internal/ComposeContract.kt.ft @@ -32,6 +32,7 @@ object ${NAME}${NAVIGATION_CLASS_SUFFIX} : Destination * ${NAME} Actions emitted from the UI Layer * passed to the coordinator to handle **/ +#if (${ACTIONS_TYPE} == "Data") data class ${NAME}Actions( #if (${AI_USED} == true) ${ACTIONS} @@ -39,7 +40,17 @@ data class ${NAME}Actions( val onClick: () -> Unit = {} #end ) +#end +#if (${ACTIONS_TYPE} == "Sealed") +sealed interface ${NAME}Action { + #if (${AI_USED} == true) + ${SEALED_ACTIONS} + #else + data object OnClick : ${NAME}Action + #end +} +#end #if (${USE_PREVIEW_PARAMETER_PROVIDER} == "true") /** diff --git a/src/main/resources/fileTemplates/internal/ComposeCoordinator.kt.ft b/src/main/resources/fileTemplates/internal/ComposeCoordinator.kt.ft index f2861e0..108264b 100644 --- a/src/main/resources/fileTemplates/internal/ComposeCoordinator.kt.ft +++ b/src/main/resources/fileTemplates/internal/ComposeCoordinator.kt.ft @@ -14,14 +14,23 @@ class ${NAME}Coordinator( val viewModel: ${NAME}ViewModel ) { val screenStateFlow = viewModel.stateFlow + #if (${ACTIONS_TYPE} == "Sealed" && ${AI_USED} == "false") + fun handle(action: ${NAME}Action) { + when (action) { + ${NAME}Action.OnClick -> { /* Handle action */ } + } + } + #end - #if (${AI_USED} == "true") - ${COORDINATOR_ACTIONS} - #else - fun doStuff() { - // TODO Handle UI Action - } + #if (${ACTIONS_TYPE} == "Sealed" && ${AI_USED} == "true") + ${COORDINATOR_HANDLERS} + #end + + #if (${AI_USED} == "true" && ${ACTIONS_TYPE} == "Data") + ${COORDINATOR_ACTIONS} #end + + } @Composable diff --git a/src/main/resources/fileTemplates/internal/ComposeRoute.kt.ft b/src/main/resources/fileTemplates/internal/ComposeRoute.kt.ft index ac74177..a477d7f 100644 --- a/src/main/resources/fileTemplates/internal/ComposeRoute.kt.ft +++ b/src/main/resources/fileTemplates/internal/ComposeRoute.kt.ft @@ -30,13 +30,30 @@ fun ${NAME}Route( #end // UI Actions + #if (${ACTIONS_TYPE} == "Data") val actions = remember${NAME}Actions(coordinator) + #end + #if (${ACTIONS_TYPE} == "Sealed") + val actionsHandler: (${NAME}Action) -> Unit = { action -> + coordinator.handle(action) + } + #end + #if (${ACTIONS_TYPE} == "Data") // UI Rendering ${NAME}Screen(uiState, actions) + #end + #if (${ACTIONS_TYPE} == "Sealed") + // UI Rendering + ${NAME}Screen( + state = uiState, + onAction = actionsHandler + ) + #end } +#if (${ACTIONS_TYPE} == "Data") @Composable fun remember${NAME}Actions(coordinator: ${NAME}Coordinator): ${NAME}Actions { return remember(coordinator) { @@ -50,4 +67,5 @@ fun remember${NAME}Actions(coordinator: ${NAME}Coordinator): ${NAME}Actions { ) #end } -} \ No newline at end of file +} +#end \ No newline at end of file diff --git a/src/main/resources/fileTemplates/internal/ComposeScreen.kt.ft b/src/main/resources/fileTemplates/internal/ComposeScreen.kt.ft index d3c18df..e90bb56 100644 --- a/src/main/resources/fileTemplates/internal/ComposeScreen.kt.ft +++ b/src/main/resources/fileTemplates/internal/ComposeScreen.kt.ft @@ -8,7 +8,11 @@ import androidx.compose.ui.tooling.preview.Preview @Composable fun ${NAME}Screen( state: ${NAME}State, - actions: ${NAME}Actions, + #if (${ACTIONS_TYPE} == "Data") + actions: ${NAME}Actions + #else + onAction: (${NAME}Action) -> Unit + #end ) { // TODO UI Rendering } @@ -22,7 +26,11 @@ private fun ${NAME}ScreenPreview( ) { ${NAME}Screen( state = state, + #if (${ACTIONS_TYPE} == "Data") actions = ${NAME}Actions() + #else + onAction = {} + #end ) } #else @@ -31,7 +39,11 @@ private fun ${NAME}ScreenPreview( private fun ${NAME}ScreenPreview() { ${NAME}Screen( state = ${NAME}State(), + #if (${ACTIONS_TYPE} == "Data") actions = ${NAME}Actions() + #else + onAction = {} + #end ) } #end \ No newline at end of file