@@ -23,16 +23,9 @@ import com.google.devtools.ksp.symbol.KSDeclaration
2323import com.google.devtools.ksp.symbol.KSFunctionDeclaration
2424import com.google.devtools.ksp.symbol.KSType
2525import com.google.devtools.ksp.symbol.Visibility
26- import com.slack.circuit.CircuitContext
27- import com.slack.circuit.CircuitUiState
28- import com.slack.circuit.Navigator
29- import com.slack.circuit.Presenter
30- import com.slack.circuit.Screen
31- import com.slack.circuit.ScreenUi
32- import com.slack.circuit.Ui
33- import com.slack.circuit.codegen.annotations.CircuitInject
3426import com.squareup.anvil.annotations.ContributesMultibinding
3527import com.squareup.kotlinpoet.AnnotationSpec
28+ import com.squareup.kotlinpoet.ClassName
3629import com.squareup.kotlinpoet.CodeBlock
3730import com.squareup.kotlinpoet.FileSpec
3831import com.squareup.kotlinpoet.FunSpec
@@ -45,7 +38,6 @@ import com.squareup.kotlinpoet.STAR
4538import com.squareup.kotlinpoet.TypeName
4639import com.squareup.kotlinpoet.TypeSpec
4740import com.squareup.kotlinpoet.asClassName
48- import com.squareup.kotlinpoet.asTypeName
4941import com.squareup.kotlinpoet.joinToCode
5042import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
5143import com.squareup.kotlinpoet.ksp.toClassName
@@ -55,9 +47,18 @@ import dagger.assisted.AssistedFactory
5547import javax.inject.Inject
5648import javax.inject.Provider
5749
58- private val CIRCUIT_INJECT_ANNOTATION = CircuitInject ::class .java.canonicalName
59- private val CIRCUIT_PRESENTER = Presenter ::class .java.canonicalName
60- private val CIRCUIT_UI = Ui ::class .java.canonicalName
50+ private const val CIRCUIT_BASE_PACKAGE = " com.slack.circuit"
51+ private val MODIFIER = ClassName (" androidx.compose.ui" , " Modifier" )
52+ private val CIRCUIT_INJECT_ANNOTATION =
53+ ClassName (" $CIRCUIT_BASE_PACKAGE .codegen.annotations" , " CircuitInject" )
54+ private val CIRCUIT_PRESENTER = ClassName (CIRCUIT_BASE_PACKAGE , " Presenter" )
55+ private val CIRCUIT_PRESENTER_FACTORY = CIRCUIT_PRESENTER .nestedClass(" Factory" )
56+ private val CIRCUIT_UI = ClassName (CIRCUIT_BASE_PACKAGE , " Ui" )
57+ private val CIRCUIT_UI_FACTORY = CIRCUIT_UI .nestedClass(" Factory" )
58+ private val CIRCUIT_UI_STATE = ClassName (CIRCUIT_BASE_PACKAGE , " CircuitUiState" )
59+ private val SCREEN = ClassName (CIRCUIT_BASE_PACKAGE , " Screen" )
60+ private val NAVIGATOR = ClassName (CIRCUIT_BASE_PACKAGE , " Navigator" )
61+ private val CIRCUIT_CONTEXT = ClassName (CIRCUIT_BASE_PACKAGE , " CircuitContext" )
6162private const val FACTORY = " Factory"
6263
6364@AutoService(SymbolProcessorProvider ::class )
@@ -68,9 +69,11 @@ public class CircuitSymbolProcessorProvider : SymbolProcessorProvider {
6869}
6970
7071private class CircuitSymbols private constructor(resolver : Resolver ) {
71- val circuitUiState = resolver.loadKSType<CircuitUiState >()
72- val screen = resolver.loadKSType<Screen >()
73- val navigator = resolver.loadKSType<Navigator >()
72+ val modifier = resolver.loadKSType(MODIFIER .canonicalName)
73+ val circuitUiState = resolver.loadKSType(CIRCUIT_UI_STATE .canonicalName)
74+ val screen = resolver.loadKSType(SCREEN .canonicalName)
75+ val navigator = resolver.loadKSType(NAVIGATOR .canonicalName)
76+
7477 companion object {
7578 fun create (resolver : Resolver ): CircuitSymbols ? {
7679 @Suppress(" SwallowedException" )
@@ -83,24 +86,23 @@ private class CircuitSymbols private constructor(resolver: Resolver) {
8386 }
8487}
8588
86- private inline fun <reified T > Resolver.loadKSType (): KSType {
87- return loadOptionalKSType<T >()
88- ? : error(" Could not find ${T ::class .java.canonicalName} in classpath" )
89- }
89+ private fun Resolver.loadKSType (name : String ): KSType =
90+ loadOptionalKSType(name) ? : error(" Could not find $name in classpath" )
9091
91- private inline fun < reified T > Resolver.loadOptionalKSType (): KSType ? {
92- return getClassDeclarationByName(getKSNameFromString( T :: class .java.canonicalName))
93- ?.asType(emptyList())
92+ private fun Resolver.loadOptionalKSType (name : String? ): KSType ? {
93+ if (name == null ) return null
94+ return getClassDeclarationByName(getKSNameFromString(name)) ?.asType(emptyList())
9495}
9596
9697private class CircuitSymbolProcessor (
9798 private val logger : KSPLogger ,
98- private val codeGenerator : CodeGenerator
99+ private val codeGenerator : CodeGenerator ,
99100) : SymbolProcessor {
100101
101102 override fun process (resolver : Resolver ): List <KSAnnotated > {
102103 val symbols = CircuitSymbols .create(resolver) ? : return emptyList()
103- resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION ).forEach { annotatedElement ->
104+ resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION .canonicalName).forEach {
105+ annotatedElement ->
104106 when (annotatedElement) {
105107 is KSClassDeclaration -> {
106108 generateFactory(annotatedElement, InstantiationType .CLASS , symbols)
@@ -121,12 +123,12 @@ private class CircuitSymbolProcessor(
121123 private fun generateFactory (
122124 annotatedElement : KSAnnotated ,
123125 instantiationType : InstantiationType ,
124- symbols : CircuitSymbols
126+ symbols : CircuitSymbols ,
125127 ) {
126128 val circuitInjectAnnotation =
127129 annotatedElement.annotations.first {
128130 it.annotationType.resolve().declaration.qualifiedName?.asString() ==
129- CIRCUIT_INJECT_ANNOTATION
131+ CIRCUIT_INJECT_ANNOTATION .canonicalName
130132 }
131133 val screenKSType = circuitInjectAnnotation.arguments[0 ].value as KSType
132134 val screenIsObject =
@@ -185,12 +187,12 @@ private class CircuitSymbolProcessor(
185187 val packageName : String ,
186188 val factoryType : FactoryType ,
187189 val constructorParams : List <ParameterSpec >,
188- val codeBlock : CodeBlock
190+ val codeBlock : CodeBlock ,
189191 )
190192
191193 /* * Computes the data needed to generate a factory. */
192194 // Detekt and ktfmt don't agree on whether or not the rectangle rule makes for readable code.
193- @Suppress(" ComplexMethod" , " LongMethod" )
195+ @Suppress(" ComplexMethod" , " LongMethod" , " ReturnCount " )
194196 @OptIn(KspExperimental ::class )
195197 private fun computeFactoryData (
196198 annotatedElement : KSAnnotated ,
@@ -232,34 +234,72 @@ private class CircuitSymbolProcessor(
232234 assistedParams
233235 )
234236 FactoryType .UI -> {
237+ // State param is optional
235238 val stateParam =
236239 fd.parameters.singleOrNull { parameter ->
237240 symbols.circuitUiState.isAssignableFrom(parameter.type.resolve())
238241 }
239- if (stateParam == null ) {
240- CodeBlock .of(
241- " %M<%T>·{·%M(%L)·}" ,
242- MemberName (" com.slack.circuit" , " ui" ),
243- CircuitUiState ::class .java,
244- MemberName (packageName, name),
245- assistedParams
246- )
247- } else {
248- val block =
249- if (assistedParams.isEmpty()) {
250- CodeBlock .of(" " )
251- } else {
252- CodeBlock .of(" ,·%L" , assistedParams)
242+
243+ // Modifier param is required
244+ val modifierParam =
245+ fd.parameters.singleOrNull { parameter ->
246+ symbols.modifier.isAssignableFrom(parameter.type.resolve())
247+ }
248+ ? : run {
249+ logger.error(" UI composable functions must have a Modifier parameter!" , fd)
250+ return null
253251 }
254- CodeBlock .of(
255- " %M<%T>·{·state·->·%M(%L·=·state%L)·}" ,
256- MemberName (" com.slack.circuit" , " ui" ),
257- stateParam.type.resolve().toTypeName(),
258- MemberName (packageName, name),
259- stateParam.name!! .getShortName(),
260- block
261- )
262- }
252+
253+ /*
254+ Diagram of what goes into generating a function!
255+ - State parameter is _optional_ and can be omitted if it's static state.
256+ - When omitted, the argument becomes _ and the param is omitted entirely.
257+ - <StateType> is either the State or CircuitUiState if no state param is used.
258+ - Modifier parameter is required.
259+ - Assisted parameters can be 0 or more extra supported assisted parameters.
260+
261+ Optional state param
262+ Optional state arg │
263+ │ │ Required modifier param
264+ │ Req modifier arg │ │
265+ ┌─── ui function │ │ │ │ Any assisted params
266+ │ │ │ Composable │ │ │
267+ │ State type │ │ │ │ │ │
268+ │ │ │ │ │ │ │ │
269+ │ │ │ │ │ │ │ │
270+ └──────┴───── ──┴── ───┴──── ──┴───── ───────┴───── ────────┴────────── ────────┴────────
271+ ui<StateType> { state, modifier -> Function(state = state, modifier = modifier, <assisted params>) }
272+ ────────────────────────────────────────────────────────────────────────────────────────────────────
273+
274+ Diagram generated with asciiflow. You can make new ones or edit with this link.
275+ https://asciiflow.com/#/share/eJzVVM1KxDAQfpVhTgr1IizLlt2CCF4F9ZhLdGclkKbd%2FMCW0rfwcXwan8SsWW27sd0qXixzmDTffN83bSY1Kp4TpspJmaDkFWlMsWa4Y5guZvOEYeWzy%2FnCZ5Z21i8Ywg%2Be29KKQnEJxnJLUHLNc8bUKIjr55jo7eXVR1R62Dplo4Xc0dYJTWvIi7XYCNIDnnpVvqjF9%2BzF2sFlsBvCCdg49bTvsVcx4vtb2u7ySlXAjRHG%2BlY%2BOjBBdYSqza6LvCwMf5Q0VS%2FqDjq%2FzVYlDfV1zPMrpWHSf6w1JeDz4HfejGCH9ybrTQT%2BUan%2FEk4s7%2Fdj%2F%2BAPUQZ1uAOSdtwuMrg5TM9ZuB9WEWb1lSawPBqL7Bwahg027%2Byjz8s%3D)
276+ */
277+
278+ @Suppress(" IfThenToElvis" ) // The elvis is less readable here
279+ val stateType =
280+ if (stateParam == null ) CIRCUIT_UI_STATE else stateParam.type.resolve().toTypeName()
281+ val stateArg = if (stateParam == null ) " _" else " state"
282+ val stateParamBlock =
283+ if (stateParam == null ) CodeBlock .of(" " )
284+ else CodeBlock .of(" %L·=·state,·" , stateParam.name!! .getShortName())
285+ val modifierParamBlock =
286+ CodeBlock .of(" %L·=·modifier" , modifierParam.name!! .getShortName())
287+ val assistedParamsBlock =
288+ if (assistedParams.isEmpty()) {
289+ CodeBlock .of(" " )
290+ } else {
291+ CodeBlock .of(" ,·%L" , assistedParams)
292+ }
293+ CodeBlock .of(
294+ " %M<%T>·{·%L,·modifier·->·%M(%L%L%L)·}" ,
295+ MemberName (" com.slack.circuit" , " ui" ),
296+ stateType,
297+ stateArg,
298+ MemberName (packageName, name),
299+ stateParamBlock,
300+ modifierParamBlock,
301+ assistedParamsBlock
302+ )
263303 }
264304 }
265305 }
@@ -291,8 +331,8 @@ private class CircuitSymbolProcessor(
291331 .getAllSuperTypes()
292332 .mapNotNull {
293333 when (it.declaration.qualifiedName?.asString()) {
294- CIRCUIT_UI -> FactoryType .UI
295- CIRCUIT_PRESENTER -> FactoryType .PRESENTER
334+ CIRCUIT_UI .canonicalName -> FactoryType .UI
335+ CIRCUIT_PRESENTER .canonicalName -> FactoryType .PRESENTER
296336 else -> null
297337 }
298338 }
@@ -361,7 +401,7 @@ private fun KSFunctionDeclaration.assistedParameters(
361401 symbols : CircuitSymbols ,
362402 logger : KSPLogger ,
363403 screenType : KSType ,
364- allowNavigator : Boolean
404+ allowNavigator : Boolean ,
365405): CodeBlock {
366406 return buildSet {
367407 for (param in parameters) {
@@ -410,22 +450,17 @@ private fun KSType.isInstanceOf(type: KSType): Boolean {
410450private fun TypeSpec.Builder.buildUiFactory (
411451 originatingSymbol : KSAnnotated ,
412452 screenBranch : CodeBlock ,
413- instantiationCodeBlock : CodeBlock
453+ instantiationCodeBlock : CodeBlock ,
414454): TypeSpec {
415- return addSuperinterface(Ui . Factory :: class )
455+ return addSuperinterface(CIRCUIT_UI_FACTORY )
416456 .addFunction(
417457 FunSpec .builder(" create" )
418458 .addModifiers(KModifier .OVERRIDE )
419- .addParameter(" screen" , Screen :: class )
420- .addParameter(" context" , CircuitContext :: class )
421- .returns(ScreenUi :: class .asClassName( ).copy(nullable = true ))
459+ .addParameter(" screen" , SCREEN )
460+ .addParameter(" context" , CIRCUIT_CONTEXT )
461+ .returns(CIRCUIT_UI .parameterizedBy( STAR ).copy(nullable = true ))
422462 .beginControlFlow(" return·when·(screen)" )
423- .addStatement(
424- " %L·->·%T(%L)" ,
425- screenBranch,
426- ScreenUi ::class .asTypeName(),
427- instantiationCodeBlock
428- )
463+ .addStatement(" %L·->·%L" , screenBranch, instantiationCodeBlock)
429464 .addStatement(" else·->·null" )
430465 .endControlFlow()
431466 .build()
@@ -437,7 +472,7 @@ private fun TypeSpec.Builder.buildUiFactory(
437472private fun TypeSpec.Builder.buildPresenterFactory (
438473 originatingSymbol : KSAnnotated ,
439474 screenBranch : CodeBlock ,
440- instantiationCodeBlock : CodeBlock
475+ instantiationCodeBlock : CodeBlock ,
441476): TypeSpec {
442477 // The TypeSpec below will generate something similar to the following.
443478 // public class AboutPresenterFactory : Presenter.Factory {
@@ -452,14 +487,14 @@ private fun TypeSpec.Builder.buildPresenterFactory(
452487 // }
453488 // }
454489
455- return addSuperinterface(Presenter . Factory :: class )
490+ return addSuperinterface(CIRCUIT_PRESENTER_FACTORY )
456491 .addFunction(
457492 FunSpec .builder(" create" )
458493 .addModifiers(KModifier .OVERRIDE )
459- .addParameter(" screen" , Screen :: class )
460- .addParameter(" navigator" , Navigator :: class )
461- .addParameter(" context" , CircuitContext :: class )
462- .returns(Presenter :: class .asClassName() .parameterizedBy(STAR ).copy(nullable = true ))
494+ .addParameter(" screen" , SCREEN )
495+ .addParameter(" navigator" , NAVIGATOR )
496+ .addParameter(" context" , CIRCUIT_CONTEXT )
497+ .returns(CIRCUIT_PRESENTER .parameterizedBy(STAR ).copy(nullable = true ))
463498 .beginControlFlow(" return when (screen)" )
464499 .addStatement(" %L·->·%L" , screenBranch, instantiationCodeBlock)
465500 .addStatement(" else·->·null" )
0 commit comments