From 9beb89d15718ccae0730bf464cc6ceeb82ac88b9 Mon Sep 17 00:00:00 2001 From: Rafael Costa Date: Sat, 18 May 2024 17:34:08 +0100 Subject: [PATCH 1/2] Fixes #611 --- .../com/ramcosta/composedestinations/utils/SpecExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/utils/SpecExtensions.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/utils/SpecExtensions.kt index 1490d9df..69b4ab98 100644 --- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/utils/SpecExtensions.kt +++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/utils/SpecExtensions.kt @@ -170,7 +170,7 @@ internal fun NavGraphSpec.addAllDestinationsTo(currentList: MutableList Date: Sat, 18 May 2024 17:35:35 +0100 Subject: [PATCH 2/2] Fixes #639 * Improved error reporting on some cases * Create dirs for graph doc files if they don't exist --- .../spec/DestinationStyleBottomSheet.kt | 7 +- .../codegen/CodeGenerator.kt | 30 ++- .../commons/DestinationWithNavArgsMapper.kt | 92 -------- .../codegen/servicelocator/ServiceLocator.kt | 3 - .../codegen/validators/InitialValidator.kt | 216 ++++++++++++++---- .../codegen/writers/MermaidGraphWriter.kt | 2 + .../writers/sub/SingleNavGraphWriter.kt | 8 +- .../wear/WearNavHostEngine.kt | 3 +- .../DefaultNavHostEngine.kt | 6 +- .../ManualComposableCalls.kt | 18 +- .../ManualComposableCallsBuilder.kt | 23 +- .../spec/DestinationStyle.kt | 16 +- .../samples/playground/AppNavigation.kt | 2 + 13 files changed, 258 insertions(+), 168 deletions(-) delete mode 100644 compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/commons/DestinationWithNavArgsMapper.kt diff --git a/compose-destinations-bottom-sheet/src/main/java/com/ramcosta/composedestinations/bottomsheet/spec/DestinationStyleBottomSheet.kt b/compose-destinations-bottom-sheet/src/main/java/com/ramcosta/composedestinations/bottomsheet/spec/DestinationStyleBottomSheet.kt index 73ebefb0..32377d24 100644 --- a/compose-destinations-bottom-sheet/src/main/java/com/ramcosta/composedestinations/bottomsheet/spec/DestinationStyleBottomSheet.kt +++ b/compose-destinations-bottom-sheet/src/main/java/com/ramcosta/composedestinations/bottomsheet/spec/DestinationStyleBottomSheet.kt @@ -11,6 +11,7 @@ import com.google.accompanist.navigation.material.bottomSheet import com.ramcosta.composedestinations.bottomsheet.scope.BottomSheetDestinationScopeImpl import com.ramcosta.composedestinations.manualcomposablecalls.DestinationLambda import com.ramcosta.composedestinations.manualcomposablecalls.ManualComposableCalls +import com.ramcosta.composedestinations.manualcomposablecalls.allDeepLinks import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.TypedDestinationSpec @@ -47,9 +48,9 @@ object DestinationStyleBottomSheet : DestinationStyle() { val contentWrapper = manualComposableCalls[destination.route] as? DestinationLambda? bottomSheet( - destination.route, - destination.arguments, - destination.deepLinks + route = destination.route, + arguments = destination.arguments, + deepLinks = destination.allDeepLinks(manualComposableCalls) ) { navBackStackEntry -> CallComposable( destination, diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/CodeGenerator.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/CodeGenerator.kt index 7db328fc..7ce4e7b2 100644 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/CodeGenerator.kt +++ b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/CodeGenerator.kt @@ -15,7 +15,6 @@ import com.ramcosta.composedestinations.codegen.model.SubModuleInfo import com.ramcosta.composedestinations.codegen.servicelocator.ServiceLocator import com.ramcosta.composedestinations.codegen.servicelocator.customNavTypeWriter import com.ramcosta.composedestinations.codegen.servicelocator.defaultKtxSerializableNavTypeSerializerWriter -import com.ramcosta.composedestinations.codegen.servicelocator.destinationWithNavArgsMapper import com.ramcosta.composedestinations.codegen.servicelocator.destinationsWriter import com.ramcosta.composedestinations.codegen.servicelocator.initialValidator import com.ramcosta.composedestinations.codegen.servicelocator.moduleOutputWriter @@ -37,7 +36,9 @@ class CodeGenerator( navTypeSerializers: List, submodules: List ) { - initialValidator.validate( + initConfigurationValues() + + val validatedDestinations: List = initialValidator.validate( navGraphs = navGraphs, destinations = destinations, submoduleResultSenders = submodules @@ -45,17 +46,28 @@ class CodeGenerator( .associateBy { it.genDestinationQualifiedName } ) - initConfigurationValues() - - val processedDestinations: List = destinationWithNavArgsMapper.map(destinations) + postValidateGenerate( + navGraphs = navGraphs, + destinations = validatedDestinations, + navTypeSerializers = navTypeSerializers, + submodules = submodules + ) + } - val navTypeNamesByType = customNavTypeWriter.write(navGraphs, processedDestinations, navTypeSerializers) + private fun postValidateGenerate( + navGraphs: List, + destinations: List, + navTypeSerializers: List, + submodules: List + ) { + val navTypeNamesByType = + customNavTypeWriter.write(navGraphs, destinations, navTypeSerializers) - moduleOutputWriter(navTypeNamesByType, submodules).write(navGraphs, processedDestinations) + moduleOutputWriter(navTypeNamesByType, submodules).write(navGraphs, destinations) - destinationsWriter(navTypeNamesByType).write(processedDestinations) + destinationsWriter(navTypeNamesByType).write(destinations) - if (shouldWriteKtxSerializableNavTypeSerializer(processedDestinations)) { + if (shouldWriteKtxSerializableNavTypeSerializer(destinations)) { defaultKtxSerializableNavTypeSerializerWriter.write() } } diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/commons/DestinationWithNavArgsMapper.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/commons/DestinationWithNavArgsMapper.kt deleted file mode 100644 index 1854c3ca..00000000 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/commons/DestinationWithNavArgsMapper.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.ramcosta.composedestinations.codegen.commons - -import com.ramcosta.composedestinations.codegen.facades.Logger -import com.ramcosta.composedestinations.codegen.model.CodeGenProcessedDestination -import com.ramcosta.composedestinations.codegen.model.DestinationGeneratingParams -import com.ramcosta.composedestinations.codegen.model.Parameter -import com.ramcosta.composedestinations.codegen.model.TypeInfo - -internal class DestinationWithNavArgsMapper { - - fun map(destinations: List): List { - return destinations.map { - CodeGenProcessedDestination( - it.getNavArgs(), - it - ) - } - } - - private fun DestinationGeneratingParams.getNavArgs(): List { - val navArgsDelegateTypeLocal = destinationNavArgsClass - return if (navArgsDelegateTypeLocal == null) { - parameters.filter { it.isNavArg() } - } else { - val nonNavArg = navArgsDelegateTypeLocal.parameters.firstOrNull { !it.isNavArg() } - if (nonNavArg != null) { - if (!nonNavArg.type.isCoreOrCustomNavArgType() && - nonNavArg.type.valueClassInnerInfo != null && // is value class - !nonNavArg.type.isValueClassOfValidInnerType()) { - - throw IllegalDestinationsSetup("Composable '${composableName}': " + - "'$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' cannot have arguments that are not navigation types. (check argument '${nonNavArg.name}')\n" + - "HINT: value classes are only valid navigation arguments if they have a public constructor with a public non nullable field which is itself of a navigation type.") - } - - throw IllegalDestinationsSetup("Composable '${composableName}': " + - "'$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' cannot have arguments that are not navigation types. (check argument '${nonNavArg.name}')") - } - - val navArgInFuncParams = parameters.firstOrNull { it.isNavArg() && it.type.value.importable != navArgsDelegateTypeLocal.type } - if (navArgInFuncParams != null) { - throw IllegalDestinationsSetup("Composable '${composableName}': annotated " + - "function cannot define arguments of navigation type if using a '$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' class. (check argument '${navArgInFuncParams.name})'") - } - - navArgsDelegateTypeLocal.parameters - } - } - - private fun Parameter.isNavArg(): Boolean { - if (isMarkedNavHostParam) { - if (!type.isNavArgType()) { - Logger.instance.info("Parameter ${this.name}: annotation @NavHostParam is redundant since it" + - " is not a navigation argument type anyway.") - } - return false - } - - return type.isNavArgType() - } - - private fun TypeInfo.isNavArgType(): Boolean { - if (isCoreOrCustomNavArgType()) { - return true - } - - return isValueClassOfValidInnerType() - } - - private fun TypeInfo.isValueClassOfValidInnerType(): Boolean { - if (valueClassInnerInfo != null && - valueClassInnerInfo.isConstructorPublic && - valueClassInnerInfo.publicNonNullableField != null - ) { - return valueClassInnerInfo.typeInfo.isCoreOrCustomNavArgType() - } - - return false - } - - private fun TypeInfo.isCoreOrCustomNavArgType(): Boolean { - if (isCoreType()) { - return true - } - - if (isCustomTypeNavArg()) { - return true - } - - return false - } -} diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/servicelocator/ServiceLocator.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/servicelocator/ServiceLocator.kt index 1c4ffc3a..a7a69268 100644 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/servicelocator/ServiceLocator.kt +++ b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/servicelocator/ServiceLocator.kt @@ -1,6 +1,5 @@ package com.ramcosta.composedestinations.codegen.servicelocator -import com.ramcosta.composedestinations.codegen.commons.DestinationWithNavArgsMapper import com.ramcosta.composedestinations.codegen.facades.CodeOutputStreamMaker import com.ramcosta.composedestinations.codegen.model.CodeGenConfig import com.ramcosta.composedestinations.codegen.model.CustomNavType @@ -74,8 +73,6 @@ internal fun ServiceLocator.navGraphsSingleObjectWriter( ::SingleNavGraphWriter ) -internal val ServiceLocator.destinationWithNavArgsMapper get() = DestinationWithNavArgsMapper() - internal val ServiceLocator.initialValidator get() = InitialValidator( codeGenConfig, isBottomSheetDependencyPresent diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/validators/InitialValidator.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/validators/InitialValidator.kt index 8468c471..3d2a3a22 100644 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/validators/InitialValidator.kt +++ b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/validators/InitialValidator.kt @@ -3,6 +3,7 @@ package com.ramcosta.composedestinations.codegen.validators import com.ramcosta.composedestinations.codegen.commons.ANIMATED_VISIBILITY_SCOPE_SIMPLE_NAME import com.ramcosta.composedestinations.codegen.commons.BOTTOM_SHEET_DEPENDENCY import com.ramcosta.composedestinations.codegen.commons.COLUMN_SCOPE_SIMPLE_NAME +import com.ramcosta.composedestinations.codegen.commons.DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT import com.ramcosta.composedestinations.codegen.commons.IllegalDestinationsSetup import com.ramcosta.composedestinations.codegen.commons.MissingRequiredDependency import com.ramcosta.composedestinations.codegen.commons.OPEN_RESULT_RECIPIENT_QUALIFIED_NAME @@ -10,20 +11,24 @@ import com.ramcosta.composedestinations.codegen.commons.RESULT_BACK_NAVIGATOR_QU import com.ramcosta.composedestinations.codegen.commons.RESULT_RECIPIENT_QUALIFIED_NAME import com.ramcosta.composedestinations.codegen.commons.firstTypeArg import com.ramcosta.composedestinations.codegen.commons.firstTypeInfoArg +import com.ramcosta.composedestinations.codegen.commons.isCoreType import com.ramcosta.composedestinations.codegen.commons.isCustomArrayOrArrayListTypeNavArg +import com.ramcosta.composedestinations.codegen.commons.isCustomTypeNavArg import com.ramcosta.composedestinations.codegen.commons.toTypeCode import com.ramcosta.composedestinations.codegen.facades.Logger import com.ramcosta.composedestinations.codegen.model.CodeGenConfig +import com.ramcosta.composedestinations.codegen.model.CodeGenProcessedDestination import com.ramcosta.composedestinations.codegen.model.DestinationGeneratingParams import com.ramcosta.composedestinations.codegen.model.DestinationResultSenderInfo import com.ramcosta.composedestinations.codegen.model.DestinationStyleType import com.ramcosta.composedestinations.codegen.model.Parameter +import com.ramcosta.composedestinations.codegen.model.RawNavArgsClass import com.ramcosta.composedestinations.codegen.model.RawNavGraphGenParams import com.ramcosta.composedestinations.codegen.model.TypeArgument import com.ramcosta.composedestinations.codegen.model.TypeInfo import com.ramcosta.composedestinations.codegen.model.Visibility -class InitialValidator( +internal class InitialValidator( private val codeGenConfig: CodeGenConfig, private val isBottomSheetDependencyPresent: Boolean ) { @@ -32,14 +37,27 @@ class InitialValidator( navGraphs: List, destinations: List, submoduleResultSenders: Map - ) { - validateNavGraphs(navGraphs) + ): List { + navGraphs.validate() + destinations.validate(navGraphs, submoduleResultSenders) + + return destinations.map { + CodeGenProcessedDestination( + it.getNavArgs(), + it + ) + } + } - val destinationsByName = lazy { destinations.associateBy { it.name } } + private fun List.validate( + navGraphs: List, + submoduleResultSenders: Map + ) { + val destinationsByName = lazy { associateBy { it.name } } val navGraphRoutes = navGraphs.map { it.baseRoute } val cleanRoutes = mutableListOf() - destinations.forEach { destination -> + forEach { destination -> destination.checkVisibilityToNavGraph() destination.checkNavArgTypes() @@ -60,8 +78,8 @@ class InitialValidator( } } - private fun validateNavGraphs(navGraphs: List) { - val navGraphsByRoute: Map> = navGraphs.groupBy { it.baseRoute } + private fun List.validate() { + val navGraphsByRoute: Map> = groupBy { it.baseRoute } navGraphsByRoute.forEach { if (it.value.size > 1) { throw IllegalDestinationsSetup( @@ -71,6 +89,54 @@ class InitialValidator( ) } } + + forEach { + it.navArgs?.validateNavArgs("NavGraph annotation '${it.annotationType.simpleName}': ") + } + } + + private fun DestinationGeneratingParams.getNavArgs(): List { + val navArgsDelegateTypeLocal = destinationNavArgsClass + return if (navArgsDelegateTypeLocal == null) { + parameters.filter { it.isNavArg() } + } else { + navArgsDelegateTypeLocal.validateNavArgs("Composable '${composableName}': ") + + val navArgInFuncParams = + parameters.firstOrNull { it.isNavArg() && it.type.value.importable != navArgsDelegateTypeLocal.type } + if (navArgInFuncParams != null) { + throw IllegalDestinationsSetup( + "Composable '${composableName}': annotated " + + "function cannot define arguments of navigation type if using a '$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' class. (check argument '${navArgInFuncParams.name})'" + ) + } + + navArgsDelegateTypeLocal.parameters + } + } + + private fun RawNavArgsClass.validateNavArgs( + errorLocationInfo: String, + ) { + val nonNavArg = parameters.firstOrNull { !it.isNavArg() } + if (nonNavArg != null) { + if (!nonNavArg.type.isCoreOrCustomNavArgType() && + nonNavArg.type.valueClassInnerInfo != null && // is value class + !nonNavArg.type.isValueClassOfValidInnerType() + ) { + + throw IllegalDestinationsSetup( + errorLocationInfo + + "'$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' cannot have arguments that are not navigation types. (check argument '${nonNavArg.name}')\n" + + "HINT: value classes are only valid navigation arguments if they have a public constructor with a public non nullable field which is itself of a navigation type." + ) + } + + throw IllegalDestinationsSetup( + errorLocationInfo + + "'$DESTINATION_ANNOTATION_NAV_ARGS_DELEGATE_ARGUMENT' cannot have arguments that are not navigation types. (check argument '${nonNavArg.name}')" + ) + } } private fun DestinationGeneratingParams.warnIgnoredAnnotationArguments() { @@ -267,8 +333,20 @@ class InitialValidator( Int::class.qualifiedName ) if (resultType.importable.qualifiedName !in primitives && !resultType.isSerializable && !resultType.isParcelable) { - throw IllegalDestinationsSetup("Composable $composableName, ${resultType.toTypeCode()}: " + - "Result types must be one of: ${listOf("String", "Long", "Boolean", "Float", "Int", "Parcelable", "java.io.Serializable").joinToString(", ")}") + throw IllegalDestinationsSetup( + "Composable $composableName, ${resultType.toTypeCode()}: " + + "Result types must be one of: ${ + listOf( + "String", + "Long", + "Boolean", + "Float", + "Int", + "Parcelable", + "java.io.Serializable" + ).joinToString(", ") + }" + ) } } @@ -279,48 +357,94 @@ class InitialValidator( ) } } -} -private fun DestinationGeneratingParams.checkNavArgTypes() { - val navArgsDelegate = destinationNavArgsClass - if (navArgsDelegate != null) { - navArgsDelegate.parameters.validateArrayTypeArgs() - } else { - parameters.validateArrayTypeArgs() - } -} -private fun List.validateArrayTypeArgs() { - val typesOfPrimitiveArrays = mapOf( - Float::class.qualifiedName to FloatArray::class, - Int::class.qualifiedName to IntArray::class, - Boolean::class.qualifiedName to BooleanArray::class, - Long::class.qualifiedName to LongArray::class, - ) - - forEach { param -> - val type = param.type - if (type.isCustomArrayOrArrayListTypeNavArg()) { - if (type.value.firstTypeInfoArg.isNullable) { - val typeArg = type.value.typeArguments.first() as TypeArgument.Typed - val recommendedType = type.copy( - value = type.value.copy( - typeArguments = listOf(typeArg.copy(type = typeArg.type.copy(isNullable = false))) - ) - ) - throw IllegalDestinationsSetup( - "'${param.name}: ${type.toTypeCode()}': " + - "Arrays / ArrayLists of nullable type arguments are not supported! Maybe " + - "'${recommendedType.toTypeCode()}' can be used instead?" + private fun Parameter.isNavArg(): Boolean { + if (isMarkedNavHostParam) { + if (!type.isNavArgType()) { + Logger.instance.info( + "Parameter ${this.name}: annotation @NavHostParam is redundant since it" + + " is not a navigation argument type anyway." ) } + return false + } - val qualifiedName = type.value.firstTypeArg.importable.qualifiedName - if (qualifiedName in typesOfPrimitiveArrays.keys) { - throw IllegalDestinationsSetup( - "'${param.name}: ${type.toTypeCode()}': " + - "${type.toTypeCode()} is not allowed, use '${typesOfPrimitiveArrays[qualifiedName]!!.simpleName}' instead." - ) + return type.isNavArgType() + } + + private fun TypeInfo.isNavArgType(): Boolean { + if (isCoreOrCustomNavArgType()) { + return true + } + + return isValueClassOfValidInnerType() + } + + private fun TypeInfo.isValueClassOfValidInnerType(): Boolean { + if (valueClassInnerInfo != null && + valueClassInnerInfo.isConstructorPublic && + valueClassInnerInfo.publicNonNullableField != null + ) { + return valueClassInnerInfo.typeInfo.isCoreOrCustomNavArgType() + } + + return false + } + + private fun TypeInfo.isCoreOrCustomNavArgType(): Boolean { + if (isCoreType()) { + return true + } + + if (isCustomTypeNavArg()) { + return true + } + + return false + } + + private fun DestinationGeneratingParams.checkNavArgTypes() { + val navArgsDelegate = destinationNavArgsClass + if (navArgsDelegate != null) { + navArgsDelegate.parameters.validateArrayTypeArgs() + } else { + parameters.validateArrayTypeArgs() + } + } + + private fun List.validateArrayTypeArgs() { + val typesOfPrimitiveArrays = mapOf( + Float::class.qualifiedName to FloatArray::class, + Int::class.qualifiedName to IntArray::class, + Boolean::class.qualifiedName to BooleanArray::class, + Long::class.qualifiedName to LongArray::class, + ) + + forEach { param -> + val type = param.type + if (type.isCustomArrayOrArrayListTypeNavArg()) { + if (type.value.firstTypeInfoArg.isNullable) { + val typeArg = type.value.typeArguments.first() as TypeArgument.Typed + val recommendedType = type.copy( + value = type.value.copy( + typeArguments = listOf(typeArg.copy(type = typeArg.type.copy(isNullable = false))) + ) + ) + throw IllegalDestinationsSetup( + "'${param.name}: ${type.toTypeCode()}': " + + "Arrays / ArrayLists of nullable type arguments are not supported! Maybe " + + "'${recommendedType.toTypeCode()}' can be used instead?" + ) + } + + val qualifiedName = type.value.firstTypeArg.importable.qualifiedName + if (qualifiedName in typesOfPrimitiveArrays.keys) { + throw IllegalDestinationsSetup( + "'${param.name}: ${type.toTypeCode()}': " + + "${type.toTypeCode()} is not allowed, use '${typesOfPrimitiveArrays[qualifiedName]!!.simpleName}' instead." + ) + } } } } diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/MermaidGraphWriter.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/MermaidGraphWriter.kt index 3ad0da04..4a7abbd2 100644 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/MermaidGraphWriter.kt +++ b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/MermaidGraphWriter.kt @@ -52,6 +52,7 @@ internal class MermaidGraphWriter( if (codeGenConfig.mermaidGraph != null) { File(codeGenConfig.mermaidGraph, "${tree.rawNavGraphGenParams.name}.mmd") + .apply { parentFile?.mkdirs() } .writeText( mermaidGraph .replace("@clicksPlaceholder@", externalNavGraphClicks(tree, "mmd")) @@ -70,6 +71,7 @@ internal class MermaidGraphWriter( val htmlMermaid = html(title, mermaidGraph) if (codeGenConfig.htmlMermaidGraph != null) { File(codeGenConfig.htmlMermaidGraph, "${tree.rawNavGraphGenParams.name}.html") + .apply { parentFile?.mkdirs() } .writeText( htmlMermaid.replace("@clicksPlaceholder@", externalNavGraphClicks(tree, "html")) ) diff --git a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/sub/SingleNavGraphWriter.kt b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/sub/SingleNavGraphWriter.kt index 595b79aa..0bc2c0e5 100644 --- a/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/sub/SingleNavGraphWriter.kt +++ b/compose-destinations-codegen/src/main/java/com/ramcosta/composedestinations/codegen/writers/sub/SingleNavGraphWriter.kt @@ -198,10 +198,10 @@ internal class SingleNavGraphWriter( } return navArgumentBridgeCodeBuilder.invokeMethodsCode( - navArgsType, - false, - directionRouteSuffix, - navArgTypes.second?.let { + navArgsType = navArgsType, + writeEmptyInvoke = false, + directionRouteSuffix = directionRouteSuffix, + additionalArgs = navArgTypes.second?.let { mapOf("startRouteArgs" to it) } ?: emptyMap() ).let { diff --git a/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt b/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt index a0e050c9..23a60291 100644 --- a/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt +++ b/compose-destinations-wear/src/main/java/com/ramcosta/composedestinations/wear/WearNavHostEngine.kt @@ -18,6 +18,7 @@ import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle import com.ramcosta.composedestinations.manualcomposablecalls.DestinationLambda import com.ramcosta.composedestinations.manualcomposablecalls.ManualComposableCalls +import com.ramcosta.composedestinations.manualcomposablecalls.allDeepLinks import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder import com.ramcosta.composedestinations.rememberNavHostEngine import com.ramcosta.composedestinations.scope.DestinationScopeImpl @@ -125,7 +126,7 @@ internal class WearNavHostEngine( composable( route = destination.route, arguments = destination.arguments, - deepLinks = destination.deepLinks + deepLinks = destination.allDeepLinks(manualComposableCalls) ) { navBackStackEntry -> CallComposable( destination, diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/DefaultNavHostEngine.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/DefaultNavHostEngine.kt index dcc279e5..d3401a28 100644 --- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/DefaultNavHostEngine.kt +++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/DefaultNavHostEngine.kt @@ -11,6 +11,7 @@ import androidx.navigation.Navigator import androidx.navigation.compose.navigation import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle import com.ramcosta.composedestinations.manualcomposablecalls.ManualComposableCalls +import com.ramcosta.composedestinations.manualcomposablecalls.allDeepLinks import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder import com.ramcosta.composedestinations.spec.NavGraphSpec import com.ramcosta.composedestinations.spec.NavHostEngine @@ -73,13 +74,14 @@ internal class DefaultNavHostEngine( ) { val transitions = manualComposableCalls.manualAnimation(navGraph.route) ?: navGraph.defaultTransitions + if (transitions != null) { with(transitions) { navigation( startDestination = navGraph.startRoute.route, route = navGraph.route, arguments = navGraph.arguments, - deepLinks = navGraph.deepLinks, + deepLinks = navGraph.allDeepLinks(manualComposableCalls), enterTransition = enterTransition, exitTransition = exitTransition, popEnterTransition = popEnterTransition, @@ -92,7 +94,7 @@ internal class DefaultNavHostEngine( startDestination = navGraph.startRoute.route, route = navGraph.route, arguments = navGraph.arguments, - deepLinks = navGraph.deepLinks, + deepLinks = navGraph.allDeepLinks(manualComposableCalls), builder = builder ) } diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCalls.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCalls.kt index aa9e47cc..6d26e8f8 100644 --- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCalls.kt +++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCalls.kt @@ -1,12 +1,15 @@ package com.ramcosta.composedestinations.manualcomposablecalls import androidx.annotation.RestrictTo +import androidx.navigation.NavDeepLink import com.ramcosta.composedestinations.spec.DestinationStyle +import com.ramcosta.composedestinations.spec.Route @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class ManualComposableCalls internal constructor( private val map: Map>, - private val animations: MutableMap + private val animations: Map, + private val deepLinks: Map> ) { operator fun get(route: String): DestinationLambda<*>? { @@ -16,4 +19,17 @@ class ManualComposableCalls internal constructor( fun manualAnimation(route: String): DestinationStyle.Animated? { return animations[route] } + + fun manualDeepLinks(route: String): List? { + return deepLinks[route] + } +} + +fun Route.allDeepLinks(manualComposableCalls: ManualComposableCalls?): List { + val manualDeepLinks = manualComposableCalls?.manualDeepLinks(route) + return if (manualDeepLinks != null) { + manualDeepLinks + deepLinks + } else { + deepLinks + } } \ No newline at end of file diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt index 9106639e..16931c69 100644 --- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt +++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/manualcomposablecalls/ManualComposableCallsBuilder.kt @@ -5,6 +5,9 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavDeepLinkDslBuilder +import androidx.navigation.navDeepLink import com.ramcosta.composedestinations.annotation.internal.InternalDestinationsApi import com.ramcosta.composedestinations.scope.AnimatedDestinationScope import com.ramcosta.composedestinations.scope.DestinationScope @@ -13,6 +16,7 @@ import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.NavGraphSpec import com.ramcosta.composedestinations.spec.NavHostEngine import com.ramcosta.composedestinations.spec.NavHostGraphSpec +import com.ramcosta.composedestinations.spec.Route import com.ramcosta.composedestinations.spec.TypedDestinationSpec /** @@ -81,6 +85,15 @@ class ManualComposableCallsBuilder internal constructor(@InternalDestinationsApi add(route, animation) } + /** + * Adds deep link created by [deepLinkBuilder] to this [Route] ([NavGraphSpec] or [DestinationSpec]). + * + * Useful when you need to create the deep link at runtime. + */ + infix fun Route.addDeepLink(deepLinkBuilder: NavDeepLinkDslBuilder.() -> Unit) { + add(route, navDeepLink(deepLinkBuilder)) + } + /** * Overrides the default animations of [this] [NavGraphSpec] at runtime to use [enterTransition], * [exitTransition], [popEnterTransition] and [popExitTransition]. @@ -147,6 +160,14 @@ class ManualComposableCallsBuilder internal constructor(@InternalDestinationsApi private val map: MutableMap> = mutableMapOf() private val animations: MutableMap = mutableMapOf() + private val deepLinks: MutableMap> = mutableMapOf() + + internal fun add( + route: String, + deepLink: NavDeepLink, + ) { + deepLinks.getOrPut(route) { mutableListOf() }.add(deepLink) + } internal fun add( route: String, @@ -164,5 +185,5 @@ class ManualComposableCallsBuilder internal constructor(@InternalDestinationsApi map[destination.route] = lambda } - internal fun build() = ManualComposableCalls(map, animations) + internal fun build() = ManualComposableCalls(map, animations, deepLinks) } \ No newline at end of file diff --git a/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/DestinationStyle.kt b/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/DestinationStyle.kt index 700142aa..e479e91d 100644 --- a/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/DestinationStyle.kt +++ b/compose-destinations/src/main/java/com/ramcosta/composedestinations/spec/DestinationStyle.kt @@ -16,6 +16,7 @@ import androidx.navigation.compose.dialog import com.ramcosta.composedestinations.annotation.internal.InternalDestinationsApi import com.ramcosta.composedestinations.manualcomposablecalls.DestinationLambda import com.ramcosta.composedestinations.manualcomposablecalls.ManualComposableCalls +import com.ramcosta.composedestinations.manualcomposablecalls.allDeepLinks import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder import com.ramcosta.composedestinations.scope.AnimatedDestinationScopeImpl import com.ramcosta.composedestinations.scope.DestinationScopeImpl @@ -55,7 +56,7 @@ abstract class DestinationStyle { composable( route = destination.route, arguments = destination.arguments, - deepLinks = destination.deepLinks, + deepLinks = destination.allDeepLinks(manualComposableCalls), ) { navBackStackEntry -> CallComposable( destination, @@ -105,7 +106,7 @@ abstract class DestinationStyle { composable( route = destination.route, arguments = destination.arguments, - deepLinks = destination.deepLinks, + deepLinks = destination.allDeepLinks(manualComposableCalls), enterTransition = enterTransition, exitTransition = exitTransition, popEnterTransition = popEnterTransition, @@ -150,7 +151,7 @@ abstract class DestinationStyle { dialog( destination.route, destination.arguments, - destination.deepLinks, + destination.allDeepLinks(manualComposableCalls), properties ) { navBackStackEntry -> CallDialogComposable( @@ -174,10 +175,13 @@ abstract class DestinationStyle { ) { destination as ActivityDestinationSpec - addComposable(destination) + addComposable(destination, manualComposableCalls) } - internal fun NavGraphBuilder.addComposable(destination: ActivityDestinationSpec) { + internal fun NavGraphBuilder.addComposable( + destination: ActivityDestinationSpec, + manualComposableCalls: ManualComposableCalls? = null + ) { activity(destination.route) { targetPackage = destination.targetPackage activityClass = destination.activityClass?.kotlin @@ -185,7 +189,7 @@ abstract class DestinationStyle { data = destination.data dataPattern = destination.dataPattern - destination.deepLinks.forEach { deepLink -> + destination.allDeepLinks(manualComposableCalls).forEach { deepLink -> deepLink { action = deepLink.action uriPattern = deepLink.uriPattern diff --git a/playground/app/src/main/java/com/ramcosta/samples/playground/AppNavigation.kt b/playground/app/src/main/java/com/ramcosta/samples/playground/AppNavigation.kt index 1c4cc25f..3f33a259 100644 --- a/playground/app/src/main/java/com/ramcosta/samples/playground/AppNavigation.kt +++ b/playground/app/src/main/java/com/ramcosta/samples/playground/AppNavigation.kt @@ -81,6 +81,8 @@ fun AppNavigation( } } ) { + TestScreenDestination addDeepLink { uriPattern = "runtimeschema://${TestScreenDestination.route}" } + ProfileScreenDestination animateWith object: DestinationStyle.Animated() { override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { fadeIn(tween(5000)) }