diff --git a/circuit-codegen-annotations/dependencies/androidReleaseRuntimeClasspath.txt b/circuit-codegen-annotations/dependencies/androidReleaseRuntimeClasspath.txt index 03b734740..0b38928dd 100644 --- a/circuit-codegen-annotations/dependencies/androidReleaseRuntimeClasspath.txt +++ b/circuit-codegen-annotations/dependencies/androidReleaseRuntimeClasspath.txt @@ -44,6 +44,7 @@ com.squareup.anvil:annotations javax.inject:javax.inject org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 diff --git a/circuit-codegen-annotations/dependencies/jvmRuntimeClasspath.txt b/circuit-codegen-annotations/dependencies/jvmRuntimeClasspath.txt index 878d0555c..40efeb674 100644 --- a/circuit-codegen-annotations/dependencies/jvmRuntimeClasspath.txt +++ b/circuit-codegen-annotations/dependencies/jvmRuntimeClasspath.txt @@ -1,10 +1,30 @@ com.google.dagger:dagger com.squareup.anvil:annotations javax.inject:javax.inject +org.jetbrains.compose.animation:animation-core-desktop +org.jetbrains.compose.animation:animation-core +org.jetbrains.compose.animation:animation-desktop +org.jetbrains.compose.animation:animation +org.jetbrains.compose.foundation:foundation-desktop +org.jetbrains.compose.foundation:foundation-layout-desktop +org.jetbrains.compose.foundation:foundation-layout +org.jetbrains.compose.foundation:foundation org.jetbrains.compose.runtime:runtime-desktop org.jetbrains.compose.runtime:runtime-saveable-desktop org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui-desktop +org.jetbrains.compose.ui:ui-geometry-desktop +org.jetbrains.compose.ui:ui-geometry +org.jetbrains.compose.ui:ui-graphics-desktop +org.jetbrains.compose.ui:ui-graphics +org.jetbrains.compose.ui:ui-text-desktop +org.jetbrains.compose.ui:ui-text +org.jetbrains.compose.ui:ui-unit-desktop +org.jetbrains.compose.ui:ui-unit +org.jetbrains.compose.ui:ui-util-desktop +org.jetbrains.compose.ui:ui-util +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 @@ -14,4 +34,6 @@ org.jetbrains.kotlinx:atomicfu org.jetbrains.kotlinx:kotlinx-coroutines-bom org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm org.jetbrains.kotlinx:kotlinx-coroutines-core +org.jetbrains.skiko:skiko-awt +org.jetbrains.skiko:skiko org.jetbrains:annotations diff --git a/circuit-codegen/build.gradle.kts b/circuit-codegen/build.gradle.kts index 0577dedd6..1711eefa7 100644 --- a/circuit-codegen/build.gradle.kts +++ b/circuit-codegen/build.gradle.kts @@ -13,7 +13,5 @@ dependencies { implementation(libs.dagger) implementation(libs.kotlinpoet) implementation(libs.kotlinpoet.ksp) - implementation(projects.circuit) - implementation(projects.circuitCodegenAnnotations) implementation(libs.anvil.annotations) } diff --git a/circuit-codegen/dependencies/runtimeClasspath.txt b/circuit-codegen/dependencies/runtimeClasspath.txt index f59380e29..4bd83d88b 100644 --- a/circuit-codegen/dependencies/runtimeClasspath.txt +++ b/circuit-codegen/dependencies/runtimeClasspath.txt @@ -5,19 +5,10 @@ com.squareup.anvil:annotations com.squareup:kotlinpoet-ksp com.squareup:kotlinpoet javax.inject:javax.inject -org.jetbrains.compose.runtime:runtime-desktop -org.jetbrains.compose.runtime:runtime-saveable-desktop -org.jetbrains.compose.runtime:runtime-saveable -org.jetbrains.compose.runtime:runtime org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-reflect org.jetbrains.kotlin:kotlin-stdlib-common org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 org.jetbrains.kotlin:kotlin-stdlib -org.jetbrains.kotlinx:atomicfu-jvm -org.jetbrains.kotlinx:atomicfu -org.jetbrains.kotlinx:kotlinx-coroutines-bom -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm -org.jetbrains.kotlinx:kotlinx-coroutines-core org.jetbrains:annotations diff --git a/circuit-codegen/gradle.properties b/circuit-codegen/gradle.properties index 1fba72e7c..5d9e63f9f 100644 --- a/circuit-codegen/gradle.properties +++ b/circuit-codegen/gradle.properties @@ -1,3 +1,4 @@ POM_ARTIFACT_ID=circuit-codegen POM_NAME=Circuit (Codegen) POM_DESCRIPTION=Circuit (Codegen) +circuit.noCompose=true diff --git a/circuit-codegen/src/main/kotlin/com/slack/circuit/codegen/CircuitSymbolProcessorProvider.kt b/circuit-codegen/src/main/kotlin/com/slack/circuit/codegen/CircuitSymbolProcessorProvider.kt index 89bd0b9db..698bdc0ae 100644 --- a/circuit-codegen/src/main/kotlin/com/slack/circuit/codegen/CircuitSymbolProcessorProvider.kt +++ b/circuit-codegen/src/main/kotlin/com/slack/circuit/codegen/CircuitSymbolProcessorProvider.kt @@ -23,16 +23,9 @@ import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.Visibility -import com.slack.circuit.CircuitContext -import com.slack.circuit.CircuitUiState -import com.slack.circuit.Navigator -import com.slack.circuit.Presenter -import com.slack.circuit.Screen -import com.slack.circuit.ScreenUi -import com.slack.circuit.Ui -import com.slack.circuit.codegen.annotations.CircuitInject import com.squareup.anvil.annotations.ContributesMultibinding import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec @@ -45,7 +38,6 @@ import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asClassName -import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.joinToCode import com.squareup.kotlinpoet.ksp.addOriginatingKSFile import com.squareup.kotlinpoet.ksp.toClassName @@ -55,9 +47,18 @@ import dagger.assisted.AssistedFactory import javax.inject.Inject import javax.inject.Provider -private val CIRCUIT_INJECT_ANNOTATION = CircuitInject::class.java.canonicalName -private val CIRCUIT_PRESENTER = Presenter::class.java.canonicalName -private val CIRCUIT_UI = Ui::class.java.canonicalName +private const val CIRCUIT_BASE_PACKAGE = "com.slack.circuit" +private val MODIFIER = ClassName("androidx.compose.ui", "Modifier") +private val CIRCUIT_INJECT_ANNOTATION = + ClassName("$CIRCUIT_BASE_PACKAGE.codegen.annotations", "CircuitInject") +private val CIRCUIT_PRESENTER = ClassName(CIRCUIT_BASE_PACKAGE, "Presenter") +private val CIRCUIT_PRESENTER_FACTORY = CIRCUIT_PRESENTER.nestedClass("Factory") +private val CIRCUIT_UI = ClassName(CIRCUIT_BASE_PACKAGE, "Ui") +private val CIRCUIT_UI_FACTORY = CIRCUIT_UI.nestedClass("Factory") +private val CIRCUIT_UI_STATE = ClassName(CIRCUIT_BASE_PACKAGE, "CircuitUiState") +private val SCREEN = ClassName(CIRCUIT_BASE_PACKAGE, "Screen") +private val NAVIGATOR = ClassName(CIRCUIT_BASE_PACKAGE, "Navigator") +private val CIRCUIT_CONTEXT = ClassName(CIRCUIT_BASE_PACKAGE, "CircuitContext") private const val FACTORY = "Factory" @AutoService(SymbolProcessorProvider::class) @@ -68,9 +69,11 @@ public class CircuitSymbolProcessorProvider : SymbolProcessorProvider { } private class CircuitSymbols private constructor(resolver: Resolver) { - val circuitUiState = resolver.loadKSType() - val screen = resolver.loadKSType() - val navigator = resolver.loadKSType() + val modifier = resolver.loadKSType(MODIFIER.canonicalName) + val circuitUiState = resolver.loadKSType(CIRCUIT_UI_STATE.canonicalName) + val screen = resolver.loadKSType(SCREEN.canonicalName) + val navigator = resolver.loadKSType(NAVIGATOR.canonicalName) + companion object { fun create(resolver: Resolver): CircuitSymbols? { @Suppress("SwallowedException") @@ -83,24 +86,23 @@ private class CircuitSymbols private constructor(resolver: Resolver) { } } -private inline fun Resolver.loadKSType(): KSType { - return loadOptionalKSType() - ?: error("Could not find ${T::class.java.canonicalName} in classpath") -} +private fun Resolver.loadKSType(name: String): KSType = + loadOptionalKSType(name) ?: error("Could not find $name in classpath") -private inline fun Resolver.loadOptionalKSType(): KSType? { - return getClassDeclarationByName(getKSNameFromString(T::class.java.canonicalName)) - ?.asType(emptyList()) +private fun Resolver.loadOptionalKSType(name: String?): KSType? { + if (name == null) return null + return getClassDeclarationByName(getKSNameFromString(name))?.asType(emptyList()) } private class CircuitSymbolProcessor( private val logger: KSPLogger, - private val codeGenerator: CodeGenerator + private val codeGenerator: CodeGenerator, ) : SymbolProcessor { override fun process(resolver: Resolver): List { val symbols = CircuitSymbols.create(resolver) ?: return emptyList() - resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION).forEach { annotatedElement -> + resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION.canonicalName).forEach { + annotatedElement -> when (annotatedElement) { is KSClassDeclaration -> { generateFactory(annotatedElement, InstantiationType.CLASS, symbols) @@ -121,12 +123,12 @@ private class CircuitSymbolProcessor( private fun generateFactory( annotatedElement: KSAnnotated, instantiationType: InstantiationType, - symbols: CircuitSymbols + symbols: CircuitSymbols, ) { val circuitInjectAnnotation = annotatedElement.annotations.first { it.annotationType.resolve().declaration.qualifiedName?.asString() == - CIRCUIT_INJECT_ANNOTATION + CIRCUIT_INJECT_ANNOTATION.canonicalName } val screenKSType = circuitInjectAnnotation.arguments[0].value as KSType val screenIsObject = @@ -185,12 +187,12 @@ private class CircuitSymbolProcessor( val packageName: String, val factoryType: FactoryType, val constructorParams: List, - val codeBlock: CodeBlock + val codeBlock: CodeBlock, ) /** Computes the data needed to generate a factory. */ // Detekt and ktfmt don't agree on whether or not the rectangle rule makes for readable code. - @Suppress("ComplexMethod", "LongMethod") + @Suppress("ComplexMethod", "LongMethod", "ReturnCount") @OptIn(KspExperimental::class) private fun computeFactoryData( annotatedElement: KSAnnotated, @@ -232,34 +234,72 @@ private class CircuitSymbolProcessor( assistedParams ) FactoryType.UI -> { + // State param is optional val stateParam = fd.parameters.singleOrNull { parameter -> symbols.circuitUiState.isAssignableFrom(parameter.type.resolve()) } - if (stateParam == null) { - CodeBlock.of( - "%M<%T>·{·%M(%L)·}", - MemberName("com.slack.circuit", "ui"), - CircuitUiState::class.java, - MemberName(packageName, name), - assistedParams - ) - } else { - val block = - if (assistedParams.isEmpty()) { - CodeBlock.of("") - } else { - CodeBlock.of(",·%L", assistedParams) + + // Modifier param is required + val modifierParam = + fd.parameters.singleOrNull { parameter -> + symbols.modifier.isAssignableFrom(parameter.type.resolve()) + } + ?: run { + logger.error("UI composable functions must have a Modifier parameter!", fd) + return null } - CodeBlock.of( - "%M<%T>·{·state·->·%M(%L·=·state%L)·}", - MemberName("com.slack.circuit", "ui"), - stateParam.type.resolve().toTypeName(), - MemberName(packageName, name), - stateParam.name!!.getShortName(), - block - ) - } + + /* + Diagram of what goes into generating a function! + - State parameter is _optional_ and can be omitted if it's static state. + - When omitted, the argument becomes _ and the param is omitted entirely. + - is either the State or CircuitUiState if no state param is used. + - Modifier parameter is required. + - Assisted parameters can be 0 or more extra supported assisted parameters. + + Optional state param + Optional state arg │ + │ │ Required modifier param + │ Req modifier arg │ │ + ┌─── ui function │ │ │ │ Any assisted params + │ │ │ Composable │ │ │ + │ State type │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + └──────┴───── ──┴── ───┴──── ──┴───── ───────┴───── ────────┴────────── ────────┴──────── + ui { state, modifier -> Function(state = state, modifier = modifier, ) } + ──────────────────────────────────────────────────────────────────────────────────────────────────── + + Diagram generated with asciiflow. You can make new ones or edit with this link. + https://asciiflow.com/#/share/eJzVVM1KxDAQfpVhTgr1IizLlt2CCF4F9ZhLdGclkKbd%2FMCW0rfwcXwan8SsWW27sd0qXixzmDTffN83bSY1Kp4TpspJmaDkFWlMsWa4Y5guZvOEYeWzy%2FnCZ5Z21i8Ywg%2Be29KKQnEJxnJLUHLNc8bUKIjr55jo7eXVR1R62Dplo4Xc0dYJTWvIi7XYCNIDnnpVvqjF9%2BzF2sFlsBvCCdg49bTvsVcx4vtb2u7ySlXAjRHG%2BlY%2BOjBBdYSqza6LvCwMf5Q0VS%2FqDjq%2FzVYlDfV1zPMrpWHSf6w1JeDz4HfejGCH9ybrTQT%2BUan%2FEk4s7%2Fdj%2F%2BAPUQZ1uAOSdtwuMrg5TM9ZuB9WEWb1lSawPBqL7Bwahg027%2Byjz8s%3D) + */ + + @Suppress("IfThenToElvis") // The elvis is less readable here + val stateType = + if (stateParam == null) CIRCUIT_UI_STATE else stateParam.type.resolve().toTypeName() + val stateArg = if (stateParam == null) "_" else "state" + val stateParamBlock = + if (stateParam == null) CodeBlock.of("") + else CodeBlock.of("%L·=·state,·", stateParam.name!!.getShortName()) + val modifierParamBlock = + CodeBlock.of("%L·=·modifier", modifierParam.name!!.getShortName()) + val assistedParamsBlock = + if (assistedParams.isEmpty()) { + CodeBlock.of("") + } else { + CodeBlock.of(",·%L", assistedParams) + } + CodeBlock.of( + "%M<%T>·{·%L,·modifier·->·%M(%L%L%L)·}", + MemberName("com.slack.circuit", "ui"), + stateType, + stateArg, + MemberName(packageName, name), + stateParamBlock, + modifierParamBlock, + assistedParamsBlock + ) } } } @@ -291,8 +331,8 @@ private class CircuitSymbolProcessor( .getAllSuperTypes() .mapNotNull { when (it.declaration.qualifiedName?.asString()) { - CIRCUIT_UI -> FactoryType.UI - CIRCUIT_PRESENTER -> FactoryType.PRESENTER + CIRCUIT_UI.canonicalName -> FactoryType.UI + CIRCUIT_PRESENTER.canonicalName -> FactoryType.PRESENTER else -> null } } @@ -361,7 +401,7 @@ private fun KSFunctionDeclaration.assistedParameters( symbols: CircuitSymbols, logger: KSPLogger, screenType: KSType, - allowNavigator: Boolean + allowNavigator: Boolean, ): CodeBlock { return buildSet { for (param in parameters) { @@ -410,22 +450,17 @@ private fun KSType.isInstanceOf(type: KSType): Boolean { private fun TypeSpec.Builder.buildUiFactory( originatingSymbol: KSAnnotated, screenBranch: CodeBlock, - instantiationCodeBlock: CodeBlock + instantiationCodeBlock: CodeBlock, ): TypeSpec { - return addSuperinterface(Ui.Factory::class) + return addSuperinterface(CIRCUIT_UI_FACTORY) .addFunction( FunSpec.builder("create") .addModifiers(KModifier.OVERRIDE) - .addParameter("screen", Screen::class) - .addParameter("context", CircuitContext::class) - .returns(ScreenUi::class.asClassName().copy(nullable = true)) + .addParameter("screen", SCREEN) + .addParameter("context", CIRCUIT_CONTEXT) + .returns(CIRCUIT_UI.parameterizedBy(STAR).copy(nullable = true)) .beginControlFlow("return·when·(screen)") - .addStatement( - "%L·->·%T(%L)", - screenBranch, - ScreenUi::class.asTypeName(), - instantiationCodeBlock - ) + .addStatement("%L·->·%L", screenBranch, instantiationCodeBlock) .addStatement("else·->·null") .endControlFlow() .build() @@ -437,7 +472,7 @@ private fun TypeSpec.Builder.buildUiFactory( private fun TypeSpec.Builder.buildPresenterFactory( originatingSymbol: KSAnnotated, screenBranch: CodeBlock, - instantiationCodeBlock: CodeBlock + instantiationCodeBlock: CodeBlock, ): TypeSpec { // The TypeSpec below will generate something similar to the following. // public class AboutPresenterFactory : Presenter.Factory { @@ -452,14 +487,14 @@ private fun TypeSpec.Builder.buildPresenterFactory( // } // } - return addSuperinterface(Presenter.Factory::class) + return addSuperinterface(CIRCUIT_PRESENTER_FACTORY) .addFunction( FunSpec.builder("create") .addModifiers(KModifier.OVERRIDE) - .addParameter("screen", Screen::class) - .addParameter("navigator", Navigator::class) - .addParameter("context", CircuitContext::class) - .returns(Presenter::class.asClassName().parameterizedBy(STAR).copy(nullable = true)) + .addParameter("screen", SCREEN) + .addParameter("navigator", NAVIGATOR) + .addParameter("context", CIRCUIT_CONTEXT) + .returns(CIRCUIT_PRESENTER.parameterizedBy(STAR).copy(nullable = true)) .beginControlFlow("return when (screen)") .addStatement("%L·->·%L", screenBranch, instantiationCodeBlock) .addStatement("else·->·null") diff --git a/circuit-test/dependencies/androidReleaseRuntimeClasspath.txt b/circuit-test/dependencies/androidReleaseRuntimeClasspath.txt index fde4a58db..c4cfeec3c 100644 --- a/circuit-test/dependencies/androidReleaseRuntimeClasspath.txt +++ b/circuit-test/dependencies/androidReleaseRuntimeClasspath.txt @@ -45,6 +45,7 @@ app.cash.turbine:turbine com.google.guava:listenablefuture org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 diff --git a/circuit-test/dependencies/jvmRuntimeClasspath.txt b/circuit-test/dependencies/jvmRuntimeClasspath.txt index 89081ba8c..804bf29c4 100644 --- a/circuit-test/dependencies/jvmRuntimeClasspath.txt +++ b/circuit-test/dependencies/jvmRuntimeClasspath.txt @@ -2,10 +2,30 @@ app.cash.molecule:molecule-runtime-jvm app.cash.molecule:molecule-runtime app.cash.turbine:turbine-jvm app.cash.turbine:turbine +org.jetbrains.compose.animation:animation-core-desktop +org.jetbrains.compose.animation:animation-core +org.jetbrains.compose.animation:animation-desktop +org.jetbrains.compose.animation:animation +org.jetbrains.compose.foundation:foundation-desktop +org.jetbrains.compose.foundation:foundation-layout-desktop +org.jetbrains.compose.foundation:foundation-layout +org.jetbrains.compose.foundation:foundation org.jetbrains.compose.runtime:runtime-desktop org.jetbrains.compose.runtime:runtime-saveable-desktop org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui-desktop +org.jetbrains.compose.ui:ui-geometry-desktop +org.jetbrains.compose.ui:ui-geometry +org.jetbrains.compose.ui:ui-graphics-desktop +org.jetbrains.compose.ui:ui-graphics +org.jetbrains.compose.ui:ui-text-desktop +org.jetbrains.compose.ui:ui-text +org.jetbrains.compose.ui:ui-unit-desktop +org.jetbrains.compose.ui:ui-unit +org.jetbrains.compose.ui:ui-util-desktop +org.jetbrains.compose.ui:ui-util +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 @@ -17,4 +37,6 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm org.jetbrains.kotlinx:kotlinx-coroutines-core org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm org.jetbrains.kotlinx:kotlinx-coroutines-test +org.jetbrains.skiko:skiko-awt +org.jetbrains.skiko:skiko org.jetbrains:annotations diff --git a/circuit/build.gradle.kts b/circuit/build.gradle.kts index b6b3d43e6..b05abe1f4 100644 --- a/circuit/build.gradle.kts +++ b/circuit/build.gradle.kts @@ -20,8 +20,10 @@ kotlin { api(libs.compose.runtime) api(libs.coroutines) api(projects.backstack) + api(libs.compose.ui) } } + maybeCreate("jvmMain").apply { dependencies { api(libs.compose.foundation) } } maybeCreate("androidMain").apply { dependencies { api(libs.androidx.compose.runtime) diff --git a/circuit/dependencies/androidReleaseRuntimeClasspath.txt b/circuit/dependencies/androidReleaseRuntimeClasspath.txt index cc98e83ed..4025b5add 100644 --- a/circuit/dependencies/androidReleaseRuntimeClasspath.txt +++ b/circuit/dependencies/androidReleaseRuntimeClasspath.txt @@ -41,6 +41,7 @@ androidx.versionedparcelable:versionedparcelable com.google.guava:listenablefuture org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 diff --git a/circuit/dependencies/jvmRuntimeClasspath.txt b/circuit/dependencies/jvmRuntimeClasspath.txt index 8028160fe..5de29d247 100644 --- a/circuit/dependencies/jvmRuntimeClasspath.txt +++ b/circuit/dependencies/jvmRuntimeClasspath.txt @@ -1,7 +1,27 @@ +org.jetbrains.compose.animation:animation-core-desktop +org.jetbrains.compose.animation:animation-core +org.jetbrains.compose.animation:animation-desktop +org.jetbrains.compose.animation:animation +org.jetbrains.compose.foundation:foundation-desktop +org.jetbrains.compose.foundation:foundation-layout-desktop +org.jetbrains.compose.foundation:foundation-layout +org.jetbrains.compose.foundation:foundation org.jetbrains.compose.runtime:runtime-desktop org.jetbrains.compose.runtime:runtime-saveable-desktop org.jetbrains.compose.runtime:runtime-saveable org.jetbrains.compose.runtime:runtime +org.jetbrains.compose.ui:ui-desktop +org.jetbrains.compose.ui:ui-geometry-desktop +org.jetbrains.compose.ui:ui-geometry +org.jetbrains.compose.ui:ui-graphics-desktop +org.jetbrains.compose.ui:ui-graphics +org.jetbrains.compose.ui:ui-text-desktop +org.jetbrains.compose.ui:ui-text +org.jetbrains.compose.ui:ui-unit-desktop +org.jetbrains.compose.ui:ui-unit +org.jetbrains.compose.ui:ui-util-desktop +org.jetbrains.compose.ui:ui-util +org.jetbrains.compose.ui:ui org.jetbrains.kotlin:kotlin-bom org.jetbrains.kotlin:kotlin-stdlib-jdk7 org.jetbrains.kotlin:kotlin-stdlib-jdk8 @@ -11,4 +31,6 @@ org.jetbrains.kotlinx:atomicfu org.jetbrains.kotlinx:kotlinx-coroutines-bom org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm org.jetbrains.kotlinx:kotlinx-coroutines-core +org.jetbrains.skiko:skiko-awt +org.jetbrains.skiko:skiko org.jetbrains:annotations diff --git a/circuit/src/androidMain/kotlin/com/slack/circuit/NavigableCircuitContent.android.kt b/circuit/src/androidMain/kotlin/com/slack/circuit/NavigableCircuitContent.android.kt index da5202afb..0e671d282 100644 --- a/circuit/src/androidMain/kotlin/com/slack/circuit/NavigableCircuitContent.android.kt +++ b/circuit/src/androidMain/kotlin/com/slack/circuit/NavigableCircuitContent.android.kt @@ -12,8 +12,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.with -import androidx.compose.foundation.background -import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -24,8 +22,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle import com.slack.circuit.backstack.BackStack import com.slack.circuit.backstack.NavDecoration import com.slack.circuit.backstack.ProvidedValues @@ -41,7 +37,8 @@ public fun NavigableCircuitContent( enableBackHandler: Boolean = true, providedValues: Map = providedValuesForBackStack(backstack), decoration: NavDecoration = NavigatorDefaults.DefaultDecoration, - unavailableRoute: @Composable (String) -> Unit = NavigatorDefaults.UnavailableRoute, + unavailableRoute: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = + circuitConfig.onUnavailableContent, ) { BackHandler(enabled = enableBackHandler && backstack.size > 1, onBack = navigator::pop) @@ -64,17 +61,17 @@ public fun BasicNavigableCircuitContent( modifier: Modifier = Modifier, circuitConfig: CircuitConfig = requireNotNull(LocalCircuitConfig.current), decoration: NavDecoration = NavigatorDefaults.EmptyDecoration, - unavailableRoute: @Composable (String) -> Unit = NavigatorDefaults.UnavailableRoute, + unavailableRoute: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = + circuitConfig.onUnavailableContent, ) { val activeContentProviders = buildList { for (record in backstack) { val provider = key(record.key) { - val routeName = record.route val screen = record.screen val currentContent: (@Composable (SaveableBackStack.Record) -> Unit) = { - CircuitContent(screen, navigator, circuitConfig) { unavailableRoute(routeName) } + CircuitContent(screen, modifier, navigator, circuitConfig, unavailableRoute) } val currentRouteContent by rememberUpdatedState(currentContent) @@ -157,16 +154,4 @@ public object NavigatorDefaults { content(arg) } } - - /** - * Bright ugly error text telling a developer they didn't provide a route that a [BackStack] asked - * for. - */ - public val UnavailableRoute: @Composable (String) -> Unit = { route -> - BasicText( - "Route not available: $route", - Modifier.background(Color.Red), - style = TextStyle(color = Color.Yellow) - ) - } } diff --git a/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitConfig.kt b/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitConfig.kt index cae85b015..c915c5f0d 100644 --- a/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitConfig.kt +++ b/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitConfig.kt @@ -2,8 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 package com.slack.circuit +import androidx.compose.foundation.background +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle /** * [CircuitConfig] adapts [presenter factories][Presenter.Factory] to their corresponding renderable @@ -53,7 +58,7 @@ import androidx.compose.runtime.Immutable public class CircuitConfig private constructor(builder: Builder) { private val uiFactories: List = builder.uiFactories.toList() private val presenterFactories: List = builder.presenterFactories.toList() - public val onUnavailableContent: (@Composable (screen: Screen) -> Unit)? = + public val onUnavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = builder.onUnavailableContent internal val eventListenerFactory: EventListener.Factory? = builder.eventListenerFactory @@ -82,11 +87,11 @@ public class CircuitConfig private constructor(builder: Builder) { return null } - public fun ui(screen: Screen, context: CircuitContext = CircuitContext(null, this)): ScreenUi? { + public fun ui(screen: Screen, context: CircuitContext = CircuitContext(null, this)): Ui<*>? { return nextUi(null, screen, context) } - public fun nextUi(skipPast: Ui.Factory?, screen: Screen, context: CircuitContext): ScreenUi? { + public fun nextUi(skipPast: Ui.Factory?, screen: Screen, context: CircuitContext): Ui<*>? { val start = uiFactories.indexOf(skipPast) + 1 for (i in start until uiFactories.size) { val ui = uiFactories[i].create(screen, context) @@ -103,7 +108,8 @@ public class CircuitConfig private constructor(builder: Builder) { public class Builder constructor() { public val uiFactories: MutableList = mutableListOf() public val presenterFactories: MutableList = mutableListOf() - public var onUnavailableContent: (@Composable (screen: Screen) -> Unit)? = null + public var onUnavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = + UnavailableContent private set public var eventListenerFactory: EventListener.Factory? = null private set @@ -140,10 +146,9 @@ public class CircuitConfig private constructor(builder: Builder) { presenterFactories.addAll(factories) } - public fun setOnUnavailableContent(content: @Composable (screen: Screen) -> Unit): Builder = - apply { - onUnavailableContent = content - } + public fun setOnUnavailableContent( + content: @Composable (screen: Screen, modifier: Modifier) -> Unit + ): Builder = apply { onUnavailableContent = content } public fun eventListenerFactory(factory: EventListener.Factory): Builder = apply { eventListenerFactory = factory @@ -154,3 +159,12 @@ public class CircuitConfig private constructor(builder: Builder) { } } } + +private val UnavailableContent: @Composable (screen: Screen, modifier: Modifier) -> Unit = + { screen, modifier -> + BasicText( + "Route not available: ${screen.javaClass.name}", + modifier.background(Color.Red), + style = TextStyle(color = Color.Yellow) + ) + } diff --git a/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitContent.kt b/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitContent.kt index 0a79eeece..fd0699d67 100644 --- a/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitContent.kt +++ b/circuit/src/commonMain/kotlin/com/slack/circuit/CircuitContent.kt @@ -7,22 +7,27 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier @Composable public fun CircuitContent( screen: Screen, + modifier: Modifier = Modifier, circuitConfig: CircuitConfig = requireNotNull(LocalCircuitConfig.current), - unavailableContent: (@Composable (screen: Screen) -> Unit)? = circuitConfig.onUnavailableContent, + unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = + circuitConfig.onUnavailableContent, ) { - CircuitContent(screen, Navigator.NoOp, circuitConfig, unavailableContent) + CircuitContent(screen, modifier, Navigator.NoOp, circuitConfig, unavailableContent) } @Composable public fun CircuitContent( screen: Screen, + modifier: Modifier = Modifier, onNavEvent: (event: NavEvent) -> Unit, circuitConfig: CircuitConfig = requireNotNull(LocalCircuitConfig.current), - unavailableContent: (@Composable (screen: Screen) -> Unit)? = circuitConfig.onUnavailableContent, + unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit) = + circuitConfig.onUnavailableContent, ) { val navigator = remember(onNavEvent) { @@ -42,30 +47,32 @@ public fun CircuitContent( } } } - CircuitContent(screen, navigator, circuitConfig, unavailableContent) + CircuitContent(screen, modifier, navigator, circuitConfig, unavailableContent) } @Composable internal fun CircuitContent( screen: Screen, + modifier: Modifier, navigator: Navigator, circuitConfig: CircuitConfig, - unavailableContent: (@Composable (screen: Screen) -> Unit)?, + unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit), ) { val parent = LocalCircuitContext.current val context = remember(screen, navigator, circuitConfig, parent) { CircuitContext(parent, circuitConfig) } CompositionLocalProvider(LocalCircuitContext provides context) { - CircuitContent(screen, navigator, circuitConfig, unavailableContent, context) + CircuitContent(screen, modifier, navigator, circuitConfig, unavailableContent, context) } } @Composable internal fun CircuitContent( screen: Screen, + modifier: Modifier, navigator: Navigator, circuitConfig: CircuitConfig, - unavailableContent: (@Composable (screen: Screen) -> Unit)?, + unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit), context: CircuitContext, ) { val eventListener = @@ -83,23 +90,22 @@ internal fun CircuitContent( eventListener.onAfterCreatePresenter(screen, navigator, presenter, context) eventListener.onBeforeCreateUi(screen, context) - val screenUi = circuitConfig.ui(screen, context) - eventListener.onAfterCreateUi(screen, screenUi, context) + val ui = circuitConfig.ui(screen, context) + eventListener.onAfterCreateUi(screen, ui, context) - if (screenUi != null && presenter != null) { + if (ui != null && presenter != null) { @Suppress("UNCHECKED_CAST") - CircuitContent(screen, eventListener, presenter, screenUi.ui as Ui) - } else if (unavailableContent != null) { - eventListener.onUnavailableContent(screen, presenter, screenUi, context) - unavailableContent(screen) + CircuitContent(screen, modifier, eventListener, presenter, ui as Ui) } else { - error("Could not render screen $screen") + eventListener.onUnavailableContent(screen, presenter, ui, context) + unavailableContent(screen, modifier) } } @Composable private fun CircuitContent( screen: Screen, + modifier: Modifier, eventListener: EventListener, presenter: Presenter, ui: Ui, @@ -117,5 +123,5 @@ private fun CircuitContent( onDispose { eventListener.onDisposeContent() } } - ui.Content(state) + ui.Content(state, modifier) } diff --git a/circuit/src/commonMain/kotlin/com/slack/circuit/EventListener.kt b/circuit/src/commonMain/kotlin/com/slack/circuit/EventListener.kt index b423a9e68..8e7f92b54 100644 --- a/circuit/src/commonMain/kotlin/com/slack/circuit/EventListener.kt +++ b/circuit/src/commonMain/kotlin/com/slack/circuit/EventListener.kt @@ -30,16 +30,15 @@ public interface EventListener { public fun onBeforeCreateUi(screen: Screen, context: CircuitContext) {} /** - * Called just after creating a [screenUi] for a given [screen]. The ui may be null if none was - * found. + * Called just after creating a [ui] for a given [screen]. The ui may be null if none was found. */ - public fun onAfterCreateUi(screen: Screen, screenUi: ScreenUi?, context: CircuitContext) {} + public fun onAfterCreateUi(screen: Screen, ui: Ui<*>?, context: CircuitContext) {} - /** Called when no content was found and one or both of [presenter] and [screenUi] are null. */ + /** Called when no content was found and one or both of [presenter] and [ui] are null. */ public fun onUnavailableContent( screen: Screen, presenter: Presenter<*>?, - screenUi: ScreenUi?, + ui: Ui<*>?, context: CircuitContext ) {} diff --git a/circuit/src/commonMain/kotlin/com/slack/circuit/Ui.kt b/circuit/src/commonMain/kotlin/com/slack/circuit/Ui.kt index 9bfb119fb..8a8d48a74 100644 --- a/circuit/src/commonMain/kotlin/com/slack/circuit/Ui.kt +++ b/circuit/src/commonMain/kotlin/com/slack/circuit/Ui.kt @@ -3,6 +3,7 @@ package com.slack.circuit import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier /** * Represents a composable UI for the given [UiState]. Conventionally, this should just be the @@ -15,11 +16,11 @@ import androidx.compose.runtime.Composable * * Usage: * ``` - * internal fun tacoUi(): Ui = ui { state -> - * Tacos(state) + * internal fun tacoUi(): Ui = ui { state, modifier -> + * Tacos(state, modifier) * } * - * @Composable private fun Tacos(state: State) {...} + * @Composable private fun Tacos(state: State, modifier: Modifier = Modifier) {...} * * @Preview * @Composable @@ -38,11 +39,10 @@ import androidx.compose.runtime.Composable * @see ui */ public interface Ui { - @Composable public fun Content(state: UiState) + @Composable public fun Content(state: UiState, modifier: Modifier) /** - * A factory that creates [ScreenUis][ScreenUi], which in turn contain the desired [Ui] for a - * given [Screen]. + * A factory that creates [Ui's][Ui] for a given [Screen]. * * Note that individual UIs should just be top-level [ui] function calls that factories simply * call into. This allows easily standing up composable preview functions. @@ -52,32 +52,25 @@ public interface Ui { * override fun create( * screen: Screen, * context: CircuitContext - * ): ScreenUi? { - * val ui = when (screen) { - * is AddFavorites -> { - * addFavoritesUi() - * } - * else -> return null + * ): Ui<*>? { + * return when (screen) { + * is AddFavorites -> addFavoritesUi() + * else -> null * } - * return ScreenUi( - * ui = ui as Ui<*, *>, - * ) * } * } * * private fun addFavoritesUi() = - * ui { state -> Favorites(state) } + * ui { state, modifier -> Favorites(state, modifier) } * - * @Composable private fun Favorites(state: State) {...} + * @Composable private fun Favorites(state: State, modifier: Modifier = Modifier) {...} * ``` */ public fun interface Factory { - public fun create(screen: Screen, context: CircuitContext): ScreenUi? + public fun create(screen: Screen, context: CircuitContext): Ui<*>? } } -public data class ScreenUi(val ui: Ui<*>) - /** * Due to this bug in Studio, we can't write lambda impls of [Ui] directly. This works around it by * offering a shim function of the same name. Once it's fixed, we can remove this and make [Ui] a @@ -88,12 +81,12 @@ public data class ScreenUi(val ui: Ui<*>) * @see [Ui] for main docs. */ public inline fun ui( - crossinline body: @Composable (state: UiState) -> Unit + crossinline body: @Composable (state: UiState, modifier: Modifier) -> Unit ): Ui { return object : Ui { @Composable - override fun Content(state: UiState) { - body(state) + override fun Content(state: UiState, modifier: Modifier) { + body(state, modifier) } } } diff --git a/circuit/src/jvmTest/kotlin/com/slack/circuit/EventListenerTest.kt b/circuit/src/jvmTest/kotlin/com/slack/circuit/EventListenerTest.kt index 07d7d7e43..389e882da 100644 --- a/circuit/src/jvmTest/kotlin/com/slack/circuit/EventListenerTest.kt +++ b/circuit/src/jvmTest/kotlin/com/slack/circuit/EventListenerTest.kt @@ -5,6 +5,7 @@ package com.slack.circuit import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier import app.cash.molecule.RecompositionClock.Immediate import app.cash.molecule.launchMolecule import app.cash.turbine.Turbine @@ -36,7 +37,7 @@ class EventListenerTest { val circuitConfig = CircuitConfig.Builder() .addPresenterFactory { _, _, _ -> presenter } - .addUiFactory { _, _ -> ScreenUi(ui) } + .addUiFactory { _, _ -> ui } .eventListenerFactory(eventListenerFactory) .build() @@ -65,7 +66,7 @@ private class StringPresenter(val state: State) : Presenter private class StringUi : Ui { - @Composable override fun Content(state: StringState) {} + @Composable override fun Content(state: StringState, modifier: Modifier) {} } private class RecordingEventListener(private val onDispose: () -> Unit) : EventListener { @@ -97,17 +98,17 @@ private class RecordingEventListener(private val onDispose: () -> Unit) : EventL log("onBeforeCreateUi: $screen") } - override fun onAfterCreateUi(screen: Screen, screenUi: ScreenUi?, context: CircuitContext) { - log("onAfterCreateUi: $screen, $screenUi") + override fun onAfterCreateUi(screen: Screen, ui: Ui<*>?, context: CircuitContext) { + log("onAfterCreateUi: $screen, $ui") } override fun onUnavailableContent( screen: Screen, presenter: Presenter<*>?, - screenUi: ScreenUi?, + ui: Ui<*>?, context: CircuitContext, ) { - error("onUnavailableContent: $screen, $presenter, $screenUi") + error("onUnavailableContent: $screen, $presenter, $ui") } override fun onStartPresent() { diff --git a/docs/factories.md b/docs/factories.md index 863640b80..243ab9b4e 100644 --- a/docs/factories.md +++ b/docs/factories.md @@ -31,19 +31,19 @@ class FavoritesScreenPresenterFactory @Inject constructor( UI factories are similar, but generally should not aggregate other UIs unless there’s a DI-specific reason to do so (which there usually isn’t!). ```kotlin -class FavoritesScreenUiFactory : @Inject constructor() : Ui.Factory { - override fun create(screen: Screen): ScreenUi? { +class FavoritesScreenUiFactory @Inject constructor() : Ui.Factory { + override fun create(screen: Screen): Ui<*>? { return when (screen) { - is FavoritesScreen -> ScreenUi(favoritesUi()) - else null -> + is FavoritesScreen -> favoritesUi() + else -> null } } } -private fun favoritesUi() = ui { state -> Favorites(state) } +private fun favoritesUi() = ui { state, modifier -> Favorites(state, modifier) } ``` !!! info - Note how these return a `ScreenUi` class that holds the Ui instance. We are using this indirection as a toe-hold for possible other future UI metadata, such as `Modifier` instances. + Note how these include a `Modifier`. You should pass on these modifiers to your UI. [Always provide a modifier!](https://chris.banes.me/posts/always-provide-a-modifier/) -We canonically write these out as a separate function (`favoritesUi()`) that returns a `Ui`, which in turn calls through to the real (basic) Compose UI function (`Favorites()`). This ensures our basic compose functions are top-level and accessible by tests, and also discourages storing anything in class members rather than idiomatic composable state vars. +We canonically write these out as a separate function (`favoritesUi()`) that returns a `Ui`, which in turn calls through to the real (basic) Compose UI function (`Favorites()`). This ensures our basic compose functions are top-level and accessible by tests, and also discourages storing anything in class members rather than idiomatic composable state vars. If you use code gen, it handles the intermediate function for you. diff --git a/docs/interop.md b/docs/interop.md index c3af160da..9528816dc 100644 --- a/docs/interop.md +++ b/docs/interop.md @@ -21,7 +21,7 @@ You can wrap your view in an `AndroidView` in a custom `Ui` implementation. ```kotlin class ExistingCustomViewUi : Ui { @Composable - fun Content(state: State) { + fun Content(state: State, modifier: Modifier = Modifier) { AndroidView( modifier = ... factory = { context -> diff --git a/docs/navigation.md b/docs/navigation.md index 17b066256..9d512d802 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -48,8 +48,8 @@ setContent { Navigation carries special semantic value in `CircuitContent` as well, where it’s common for UIs to want to curry navigation events emitted by nested UIs. For this case, there’s a `CircuitContent` overload that accepts an optional onNavEvent callback that you must then forward to a Navigator instance. ```kotlin -@Composable fun ParentUi(state: ParentState) { - CircuitContent(NestedScreen, onNavEvent = { navEvent -> state.eventSink(NestedNav(navEvent)) }) +@Composable fun ParentUi(state: ParentState, modifier: Modifier = Modifier) { + CircuitContent(NestedScreen, modifier = modifier, onNavEvent = { navEvent -> state.eventSink(NestedNav(navEvent)) }) } @Composable fun ParentPresenter(navigator: Navigator): ParentState { diff --git a/docs/ui.md b/docs/ui.md index 26e7491da..e20db64a8 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -5,7 +5,7 @@ The core Ui interface is simply this: ```kotlin interface Ui { - @Composable fun Content(state: UiState) + @Composable fun Content(state: UiState, modifier: Modifier) } ``` @@ -14,7 +14,7 @@ Like presenters, simple UIs can also skip the class all together for use in othe ```kotlin @CircuitInject // Relevant DI wiring is generated @Composable -private fun Favorites(state: FavoritesState) { +private fun Favorites(state: FavoritesState, modifier: Modifier = Modifier) { // ... } ``` @@ -31,11 +31,11 @@ Let’s look a little more closely at the last bullet point about preview functi ```kotlin @Preview @Composable -private fun PreviewFavorites() = Favorites(FavoritesState(listOf("Reeses", "Lola")) +private fun PreviewFavorites() = Favorites(FavoritesState(listOf("Reeses", "Lola"))) @Preview @Composable -private fun PreviewEmptyFavorites() = Favorites(FavoritesState(listOf()) +private fun PreviewEmptyFavorites() = Favorites(FavoritesState(listOf())) ``` TODO image sample of IDE preview diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41a5a08fd..38974450f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -143,6 +143,8 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-jb" } compose-runtime-saveable = { module = "org.jetbrains.compose.runtime:runtime-saveable", version.ref = "compose-jb" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-jb" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-jb" } coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } diff --git a/samples/counter/android/src/main/kotlin/com/slack/circuit/sample/counter/android/AndroidCounterCircuit.kt b/samples/counter/android/src/main/kotlin/com/slack/circuit/sample/counter/android/AndroidCounterCircuit.kt index b4723b64a..136b11851 100644 --- a/samples/counter/android/src/main/kotlin/com/slack/circuit/sample/counter/android/AndroidCounterCircuit.kt +++ b/samples/counter/android/src/main/kotlin/com/slack/circuit/sample/counter/android/AndroidCounterCircuit.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.slack.circuit.CircuitContext import com.slack.circuit.Screen -import com.slack.circuit.ScreenUi import com.slack.circuit.Ui import com.slack.circuit.sample.counter.CounterEvent import com.slack.circuit.sample.counter.CounterScreen @@ -66,9 +65,9 @@ fun Counter(state: CounterState, modifier: Modifier = Modifier) { } class CounterUiFactory : Ui.Factory { - override fun create(screen: Screen, context: CircuitContext): ScreenUi? { + override fun create(screen: Screen, context: CircuitContext): Ui<*>? { return when (screen) { - is CounterScreen -> ScreenUi(ui { state -> Counter(state) }) + is CounterScreen -> ui { state, modifier -> Counter(state, modifier) } else -> null } } diff --git a/samples/counter/desktop/src/main/kotlin/com/slack/circuit/sample/counter/desktop/DesktopCounterCircuit.kt b/samples/counter/desktop/src/main/kotlin/com/slack/circuit/sample/counter/desktop/DesktopCounterCircuit.kt index 31f477530..a18598c58 100644 --- a/samples/counter/desktop/src/main/kotlin/com/slack/circuit/sample/counter/desktop/DesktopCounterCircuit.kt +++ b/samples/counter/desktop/src/main/kotlin/com/slack/circuit/sample/counter/desktop/DesktopCounterCircuit.kt @@ -27,7 +27,6 @@ import com.slack.circuit.CircuitConfig import com.slack.circuit.CircuitContent import com.slack.circuit.CircuitContext import com.slack.circuit.Screen -import com.slack.circuit.ScreenUi import com.slack.circuit.Ui import com.slack.circuit.sample.counter.CounterEvent import com.slack.circuit.sample.counter.CounterPresenterFactory @@ -67,9 +66,9 @@ fun Counter(state: CounterState, modifier: Modifier = Modifier) { } class CounterUiFactory : Ui.Factory { - override fun create(screen: Screen, context: CircuitContext): ScreenUi? { + override fun create(screen: Screen, context: CircuitContext): Ui<*>? { return when (screen) { - is CounterScreen -> ScreenUi(ui { state -> Counter(state) }) + is CounterScreen -> ui { state, modifier -> Counter(state, modifier) } else -> null } } diff --git a/samples/counter/mosaic/src/main/kotlin/com/slack/circuit/sample/counter/mosaic/MosaicCounterCircuit.kt b/samples/counter/mosaic/src/main/kotlin/com/slack/circuit/sample/counter/mosaic/MosaicCounterCircuit.kt index 18b6675d5..b59da53ee 100644 --- a/samples/counter/mosaic/src/main/kotlin/com/slack/circuit/sample/counter/mosaic/MosaicCounterCircuit.kt +++ b/samples/counter/mosaic/src/main/kotlin/com/slack/circuit/sample/counter/mosaic/MosaicCounterCircuit.kt @@ -19,7 +19,6 @@ import com.slack.circuit.CircuitConfig import com.slack.circuit.CircuitContent import com.slack.circuit.CircuitContext import com.slack.circuit.Screen -import com.slack.circuit.ScreenUi import com.slack.circuit.Ui import com.slack.circuit.sample.counter.CounterEvent import com.slack.circuit.sample.counter.CounterPresenterFactory @@ -53,15 +52,15 @@ fun main(args: Array) = MosaicCounterCommand().main(args) object MosaicCounterScreen : CounterScreen class CounterUiFactory : Ui.Factory { - override fun create(screen: Screen, context: CircuitContext): ScreenUi? { + override fun create(screen: Screen, context: CircuitContext): Ui<*>? { return when (screen) { - MosaicCounterScreen -> ScreenUi(counterUi()) + MosaicCounterScreen -> counterUi() else -> null } } } -private fun counterUi(): Ui = ui { state -> Counter(state) } +private fun counterUi(): Ui = ui { state, _ -> Counter(state) } @Composable private fun Counter(state: CounterState) { diff --git a/samples/interop/src/main/kotlin/com/slack/circuit/sample/interop/MainActivity.kt b/samples/interop/src/main/kotlin/com/slack/circuit/sample/interop/MainActivity.kt index f028dc04d..42d2136d1 100644 --- a/samples/interop/src/main/kotlin/com/slack/circuit/sample/interop/MainActivity.kt +++ b/samples/interop/src/main/kotlin/com/slack/circuit/sample/interop/MainActivity.kt @@ -7,7 +7,6 @@ import android.os.Parcelable import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -35,7 +34,6 @@ import com.slack.circuit.CircuitContent import com.slack.circuit.CircuitContext import com.slack.circuit.Navigator import com.slack.circuit.Presenter -import com.slack.circuit.ScreenUi import com.slack.circuit.Ui import com.slack.circuit.sample.counter.CounterPresenterFactory import com.slack.circuit.sample.counter.CounterScreen @@ -58,7 +56,7 @@ class MainActivity : AppCompatActivity() { } .addUiFactory { screen, _ -> when (screen) { - is InteropCounterScreen -> ScreenUi(screen.uiSource.createUi()) + is InteropCounterScreen -> screen.uiSource.createUi() else -> null } } @@ -89,12 +87,12 @@ class MainActivity : AppCompatActivity() { } }, ) { paddingValues -> - Box(Modifier.padding(paddingValues)) { - // TODO this is necessary because the CircuitContent caches the Ui and Presenter, which - // doesn't play well swapping out the Ui and Presenter sources. Might be nice to make - // them live enough to support this, but also sort of orthogonal to the point of this - // sample. - key(circuitScreen) { CircuitContent(screen = circuitScreen) } + // TODO this is necessary because the CircuitContent caches the Ui and Presenter, which + // doesn't play well swapping out the Ui and Presenter sources. Might be nice to make + // them live enough to support this, but also sort of orthogonal to the point of this + // sample. + key(circuitScreen) { + CircuitContent(screen = circuitScreen, modifier = Modifier.padding(paddingValues)) } } } @@ -213,12 +211,12 @@ private enum class PresenterSource { enum class UiSource { Circuit { override fun createUi(): Ui { - return ui { state -> Counter(state) } + return ui { state, modifier -> Counter(state, modifier) } } }, View { override fun createUi(): Ui { - return ui { state -> CounterViewComposable(state) } + return ui { state, modifier -> CounterViewComposable(state, modifier) } } }; diff --git a/samples/star/src/androidTest/kotlin/com/slack/circuit/star/petdetail/PetDetailTest.kt b/samples/star/src/androidTest/kotlin/com/slack/circuit/star/petdetail/PetDetailTest.kt index bc545ce82..4e8ecdac2 100644 --- a/samples/star/src/androidTest/kotlin/com/slack/circuit/star/petdetail/PetDetailTest.kt +++ b/samples/star/src/androidTest/kotlin/com/slack/circuit/star/petdetail/PetDetailTest.kt @@ -91,9 +91,9 @@ class PetDetailTest { var carouselScreen: PetPhotoCarouselScreen? = null val circuitConfig = CircuitConfig.Builder() - .setOnUnavailableContent { screen -> + .setOnUnavailableContent { screen, modifier -> carouselScreen = screen as PetPhotoCarouselScreen - PetPhotoCarousel(PetPhotoCarouselScreen.State(screen)) + PetPhotoCarousel(PetPhotoCarouselScreen.State(screen), modifier) } .build() @@ -138,8 +138,8 @@ class PetDetailTest { val circuitConfig = CircuitConfig.Builder() - .setOnUnavailableContent { screen -> - PetPhotoCarousel(PetPhotoCarouselScreen.State(screen as PetPhotoCarouselScreen)) + .setOnUnavailableContent { screen, modifier -> + PetPhotoCarousel(PetPhotoCarouselScreen.State(screen as PetPhotoCarouselScreen), modifier) } .build() diff --git a/samples/star/src/main/kotlin/com/slack/circuit/star/home/HomeScreen.kt b/samples/star/src/main/kotlin/com/slack/circuit/star/home/HomeScreen.kt index 4a78dcc38..bcec55846 100644 --- a/samples/star/src/main/kotlin/com/slack/circuit/star/home/HomeScreen.kt +++ b/samples/star/src/main/kotlin/com/slack/circuit/star/home/HomeScreen.kt @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 package com.slack.circuit.star.home -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -73,10 +72,12 @@ fun HomeContent(state: HomeScreen.State, modifier: Modifier = Modifier) { } } ) { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { - val screen = state.homeNavState.bottomNavItems[state.homeNavState.index].screen - CircuitContent(screen, { event -> eventSink(ChildNav(event)) }) - } + val screen = state.homeNavState.bottomNavItems[state.homeNavState.index].screen + CircuitContent( + screen, + modifier = Modifier.padding(paddingValues), + onNavEvent = { event -> eventSink(ChildNav(event)) } + ) } } diff --git a/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetDetail.kt b/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetDetail.kt index 332d98bdb..d832a6784 100644 --- a/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetDetail.kt +++ b/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetDetail.kt @@ -161,12 +161,12 @@ internal object PetDetailTestConstants { @CircuitInject(PetDetailScreen::class, AppScope::class) @Composable -internal fun PetDetail(state: PetDetailScreen.State) { +internal fun PetDetail(state: PetDetailScreen.State, modifier: Modifier = Modifier) { val systemUiController = rememberSystemUiController() systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background) systemUiController.setNavigationBarColor(MaterialTheme.colorScheme.background) - Scaffold(modifier = Modifier.systemBarsPadding(), topBar = { TopBar(state) }) { padding -> + Scaffold(modifier = modifier.systemBarsPadding(), topBar = { TopBar(state) }) { padding -> when (state) { is PetDetailScreen.State.Loading -> Loading(padding) is PetDetailScreen.State.UnknownAnimal -> UnknownAnimal(padding) diff --git a/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetPhotoCarousel.kt b/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetPhotoCarousel.kt index a3fb2f5a2..93c82239a 100644 --- a/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetPhotoCarousel.kt +++ b/samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetPhotoCarousel.kt @@ -110,7 +110,7 @@ internal object PetPhotoCarouselTestConstants { @CircuitInject(PetPhotoCarouselScreen::class, AppScope::class) @OptIn(ExperimentalPagerApi::class) @Composable -internal fun PetPhotoCarousel(state: PetPhotoCarouselScreen.State) { +internal fun PetPhotoCarousel(state: PetPhotoCarouselScreen.State, modifier: Modifier = Modifier) { val (name, photoUrls, photoUrlMemoryCacheKey) = state val context = LocalContext.current val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE @@ -128,7 +128,7 @@ internal fun PetPhotoCarousel(state: PetPhotoCarouselScreen.State) { val scope = rememberCoroutineScope() val requester = remember { FocusRequester() } @Suppress("MagicNumber") - val columnModifier = if (isLandscape) Modifier.fillMaxWidth(0.5f) else Modifier.fillMaxSize() + val columnModifier = if (isLandscape) modifier.fillMaxWidth(0.5f) else modifier.fillMaxSize() Column( columnModifier .testTag(CAROUSEL_TAG) diff --git a/samples/star/src/test/kotlin/com/slack/circuit/star/petdetail/PetDetailUiTest.kt b/samples/star/src/test/kotlin/com/slack/circuit/star/petdetail/PetDetailUiTest.kt index 7e830dd26..5a171a6ba 100644 --- a/samples/star/src/test/kotlin/com/slack/circuit/star/petdetail/PetDetailUiTest.kt +++ b/samples/star/src/test/kotlin/com/slack/circuit/star/petdetail/PetDetailUiTest.kt @@ -40,10 +40,10 @@ class PetDetailUiTest { private var carouselScreen: PetPhotoCarouselScreen? = null private val circuitConfig = CircuitConfig.Builder() - .setOnUnavailableContent { screen -> + .setOnUnavailableContent { screen, modifier -> when (screen) { is PetPhotoCarouselScreen -> { - PetPhotoCarousel(PetPhotoCarouselScreen.State(screen)) + PetPhotoCarousel(PetPhotoCarouselScreen.State(screen), modifier) carouselScreen = screen } } @@ -146,8 +146,8 @@ class PetDetailUiTest { val circuitConfig = CircuitConfig.Builder() - .setOnUnavailableContent { screen -> - PetPhotoCarousel(PetPhotoCarouselScreen.State(screen as PetPhotoCarouselScreen)) + .setOnUnavailableContent { screen, modifier -> + PetPhotoCarousel(PetPhotoCarouselScreen.State(screen as PetPhotoCarouselScreen), modifier) } .build()