[Compose] Update the gradle pluging for featureFlags
The compose compiler introduced a featureFlags option
and move the configuration of experimental features to
be configured by feature flags instead of options which
allows the more flexability and better error diagnostics
when testing and experimenting with features that are
being rolled out.
diff --git a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerGradlePluginExtension.kt b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerGradlePluginExtension.kt
index e78acef..a109ce8 100644
--- a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerGradlePluginExtension.kt
+++ b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerGradlePluginExtension.kt
@@ -83,6 +83,7 @@
* }
* ```
*/
+ @Deprecated("Use the featureFlags option instead")
val enableIntrinsicRemember: Property<Boolean> = objectFactory.property(Boolean::class.java).convention(true)
/**
@@ -95,6 +96,7 @@
*
* This feature is still considered experimental and is thus disabled by default.
*/
+ @Deprecated("Use the featureFlags option instead")
val enableNonSkippingGroupOptimization: Property<Boolean> = objectFactory.property(Boolean::class.java).convention(false)
/**
@@ -107,6 +109,7 @@
* For more information, see this link:
* - [AndroidX strong skipping](https://github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/strong-skipping.md)
*/
+ @Deprecated("Use the featureFlags option instead")
val enableStrongSkippingMode: Property<Boolean> = objectFactory.property(Boolean::class.java).convention(false)
/**
@@ -145,8 +148,10 @@
* composeCompiler {
* targetKotlinPlatforms.set(
* KotlinPlatformType.values()
- * .filterNot { it == KotlinPlatformType.native || it == KotlinPlatformType.js }
- * .asIterable()
+ * .filterNot {
+ * it == KotlinPlatformType.native ||
+ * it == KotlinPlatformType.js
+ * }.asIterable()
* )
* }
* ```
@@ -154,4 +159,26 @@
val targetKotlinPlatforms: SetProperty<KotlinPlatformType> = objectFactory
.setProperty(KotlinPlatformType::class.java)
.convention(KotlinPlatformType.values().asIterable())
+
+ /**
+ * A set of feature flags to enable. A feature requires a feature flags when it is in the process of becoming the default
+ * behavior of the Compose compiler. Features in this set will eventually be removed and disabling will no longer be
+ * supported. See [ComposeFeatureFlag] for the list of features currently recognized by the plugin.
+ *
+ * @see ComposeFeatureFlag
+ */
+ @Suppress("DEPRECATION")
+ val featureFlags: SetProperty<ComposeFeatureFlag> = objectFactory
+ .setProperty(ComposeFeatureFlag::class.java)
+ .convention(
+ // Add features that used to be added by deprecated options. No other features should be added this way.
+ enableIntrinsicRemember.zip(enableStrongSkippingMode) { intrinsicRemember, strongSkippingMode ->
+ setOfNotNull(
+ if (!intrinsicRemember) ComposeFeatureFlag.IntrinsicRemember.disable() else null,
+ if (strongSkippingMode) ComposeFeatureFlag.StrongSkipping else null
+ )
+ }.zip(enableNonSkippingGroupOptimization) { features, nonSkippingGroupsOptimization ->
+ if (nonSkippingGroupsOptimization) features + ComposeFeatureFlag.OptimizeNonSkippingGroups else features
+ }
+ )
}
diff --git a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
index 4173ed0..f4000ba 100644
--- a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
+++ b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
@@ -72,22 +72,17 @@
add(composeExtension.reportsDestination.map<SubpluginOption> {
FilesSubpluginOption("reportsDestination", listOf(it.asFile))
}.orElse(EMPTY_OPTION))
- add(composeExtension.enableIntrinsicRemember.map {
- SubpluginOption("intrinsicRemember", it.toString())
- })
- add(composeExtension.enableNonSkippingGroupOptimization.map {
- SubpluginOption("nonSkippingGroupOptimization", it.toString())
- })
- add(composeExtension.enableStrongSkippingMode.map {
- // Rename once the option in Compose compiler is also renamed
- SubpluginOption("strongSkipping", it.toString())
- })
+
add(composeExtension.stabilityConfigurationFile.map<SubpluginOption> {
FilesSubpluginOption("stabilityConfigurationPath", listOf(it.asFile))
}.orElse(EMPTY_OPTION))
add(composeExtension.includeTraceMarkers.map {
SubpluginOption("traceMarkersEnabled", it.toString())
})
+
+ addAll(composeExtension.featureFlags.map { flags ->
+ flags.map { SubpluginOption("featureFlag", it.toString()) }
+ }.orElse(emptyList()))
}
return project.objects
diff --git a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeFeatureFlags.kt b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeFeatureFlags.kt
new file mode 100644
index 0000000..906ddb2
--- /dev/null
+++ b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeFeatureFlags.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.compose.compiler.gradle
+
+import org.gradle.api.Named
+import java.io.Serializable
+
+/**
+ * A feature flag is used to roll out features that will eventually become the default behavior of the Compose compiler plugin.
+ *
+ * A feature flag value that disables the feature can be created by calling [disable] on the feature flag.
+ */
+sealed interface ComposeFeatureFlag : Named, Serializable {
+ /**
+ * Return a feature flag that will disable this feature.
+ */
+ fun disable(): ComposeFeatureFlag
+
+ /**
+ * The enabled value of [feature]. These values are stored in a set they should have value identity.
+ */
+ private class Enabled(val feature: Feature) : ComposeFeatureFlag, Serializable {
+ override fun disable() = Disabled(feature)
+ override fun getName(): String = feature.name
+ override fun hashCode(): Int = feature.hashCode() * 17
+ override fun equals(other: Any?): Boolean = other is Enabled && other.feature == feature
+ override fun toString(): String = feature.flag
+ }
+
+ /**
+ * The disabled value of [feature]. These values are stored in a set they should have value identity.
+ */
+ private class Disabled(val feature: Feature) : ComposeFeatureFlag, Serializable {
+ override fun disable(): ComposeFeatureFlag = this
+ override fun getName(): String = "Disabled ${feature.name}"
+ override fun hashCode(): Int = feature.hashCode() * 19
+ override fun equals(other: Any?): Boolean = other is Disabled && other.feature == feature
+ override fun toString(): String = "-${feature.flag}"
+ }
+
+ /**
+ * The enum of all feature flags known by the Gradle plugin. This should be a superset of the features
+ * known by the Compose Gradle plugin.
+ *
+ * When a feature flag is added to the compiler plugin,
+ * 1) Add an enum value of the feature flag where the [flag] value is the token used in the `featureFlags` option of the
+ * compiler plugin.
+ * 2) Add a @JvmField field in the companion object that will be accessible from Groovy adn used in build files to enable and
+ * disable the field.
+ * 3) Add a doc comment to the field to explain what the feature does and if it is enabled or disabled by default.
+ *
+ * When a feature becomes enabled by default,
+ * 1) Change the documentation for the feature flag to indicate that it is now on by default.
+ * - No other changes are required. The default enabled/disabled state of a feature is owned by the compiler plugin.
+ *
+ * When a feature flag is removed from the compiler plugin,
+ * 1) Deprecate the @JvmField field in the companion object explaining that this flag is now ignored by the compiler plugin and should
+ * be removed from the configuration.
+ * 2) Update the documentation to indicate that this flag is ignored.
+ * 3) In a subsequent version remove the enum entry and the @JvmField field.
+ */
+ private enum class Feature(val flag: String) {
+ StrongSkipping("StrongSkipping"),
+ IntrinsicRemember("IntrinsicRemember"),
+ OptimizeNonSkippingGroups("OptimizeNonSkippingGroups"),
+ }
+
+ companion object {
+ /**
+ * Enable strong skipping.
+ *
+ * Strong Skipping is a mode that improves the runtime performance of your application by skipping unnecessary
+ * invocations of composable functions for which the parameters have not changed. In particular, when enabled, Composable functions
+ * with unstable parameters become skippable and lambdas with unstable captures will be memoized.
+ *
+ * For more information, see this link:
+ * - [Strong skipping](https://https://github.com/JetBrains/kotlin/blob/master/plugins/compose/design/strong-skipping.md)
+ *
+ * This feature is disabled by default. To enable, include this feature flag,
+ * ```
+ * composeOptions {
+ * featureFlags = setOf(ComposeFeatureFlag.StrongSkipping)
+ * }
+ * ```
+ */
+ @JvmField
+ val StrongSkipping: ComposeFeatureFlag = Enabled(Feature.StrongSkipping)
+
+ /**
+ * Enable the intrinsic remember performance optimization.
+ *
+ * Intrinsic Remember is an optimization mode which improves the runtime performance of your application by inlining `remember`
+ * invocations and replacing `.equals` comparison (for keys) with comparisons of the `$changed` meta parameter when possible. This
+ * results in fewer slots being used and fewer comparisons being done at runtime.
+ *
+ * This feature is on by default. It can be disabled by adding a disable flag by calling [disable] on this flag. To disable,
+ * ```
+ * composeOptions {
+ * featureFlags = setOf(ComposeFeatureFlag.IntrinsicRemember.disable())
+ * }
+ * ```
+ */
+ @JvmField
+ val IntrinsicRemember: ComposeFeatureFlag = Enabled(Feature.IntrinsicRemember)
+
+ /**
+ * Remove groups around non-skipping composable functions.
+ *
+ * Removing groups around non-skipping composable function is an experimental mode which improves the runtime performance of your
+ * application by skipping unnecessary groups around composable functions which do not skip (and thus do not require a group). This
+ * optimization will remove the groups around functions that are not skippable such as explicitly marked as
+ * `@NonSkippableComposable` and functions that are implicitly not skippable such inline functions and functions that return a
+ * non-Unit value such as remember.
+ *
+ * This feature is still considered experimental and is thus disabled by default. To enable,
+ * ```
+ * composeOptions {
+ * featureFlags = setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups)
+ * }
+ * ```
+ */
+ @JvmField
+ val OptimizeNonSkippingGroups: ComposeFeatureFlag = Enabled(Feature.OptimizeNonSkippingGroups)
+ }
+}
diff --git a/libraries/tools/kotlin-compose-compiler/src/functionalTest/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ExtensionConfigurationTest.kt b/libraries/tools/kotlin-compose-compiler/src/functionalTest/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ExtensionConfigurationTest.kt
index f4a9960..0dd5dc1 100644
--- a/libraries/tools/kotlin-compose-compiler/src/functionalTest/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ExtensionConfigurationTest.kt
+++ b/libraries/tools/kotlin-compose-compiler/src/functionalTest/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ExtensionConfigurationTest.kt
@@ -24,9 +24,6 @@
listOf(
"generateFunctionKeyMetaClasses" to "false",
"sourceInformation" to "false",
- "intrinsicRemember" to "true",
- "nonSkippingGroupOptimization" to "false",
- "strongSkipping" to "false",
"traceMarkersEnabled" to "false",
),
options
@@ -98,6 +95,80 @@
}
}
+ @Test
+ fun disableIntrinsicRemember() {
+ testComposeFeatureFlags(listOf("-IntrinsicRemember")) { extension ->
+ extension.featureFlags.value(setOf(ComposeFeatureFlag.IntrinsicRemember.disable()))
+ }
+ }
+
+ @Test
+ fun enableStrongSkipping() {
+ testComposeFeatureFlags(listOf("StrongSkipping")) { extension ->
+ extension.featureFlags.value(setOf(ComposeFeatureFlag.StrongSkipping))
+ }
+ }
+
+ @Test
+ fun enableNonSkippingGroupOptimization() {
+ testComposeFeatureFlags(listOf("OptimizeNonSkippingGroups")) { extension ->
+ extension.featureFlags.value(setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups))
+ }
+ }
+
+ @Test
+ fun disableIntrinsicRememberCompatibility() {
+ testComposeFeatureFlags(listOf("-IntrinsicRemember")) { extension ->
+ @Suppress("DEPRECATION")
+ extension.enableIntrinsicRemember.value(false)
+ }
+ }
+
+ @Test
+ fun enableStrongSkippingCompatibility() {
+ testComposeFeatureFlags(listOf("StrongSkipping")) { extension ->
+ @Suppress("DEPRECATION")
+ extension.enableStrongSkippingMode.value(true)
+ }
+ }
+
+ @Test
+ fun enableNonSkippingGroupOptimizationCompatibility() {
+ testComposeFeatureFlags(listOf("OptimizeNonSkippingGroups")) { extension ->
+ @Suppress("DEPRECATION")
+ extension.enableNonSkippingGroupOptimization.value(true)
+ }
+ }
+
+ @Test
+ fun enableMultipleFlags() {
+ testComposeFeatureFlags(listOf("OptimizeNonSkippingGroups", "StrongSkipping", "-IntrinsicRemember")) { extension ->
+ extension.featureFlags.set(
+ setOf(
+ ComposeFeatureFlag.StrongSkipping,
+ ComposeFeatureFlag.IntrinsicRemember.disable(),
+ ComposeFeatureFlag.OptimizeNonSkippingGroups
+ )
+ )
+ }
+ }
+
+ @Test
+ fun enableMultipleFlagsCompatibility() {
+ @Suppress("DEPRECATION")
+ testComposeFeatureFlags(listOf("OptimizeNonSkippingGroups", "StrongSkipping", "-IntrinsicRemember")) { extension ->
+ extension.enableStrongSkippingMode.value(true)
+ extension.enableNonSkippingGroupOptimization.value(true)
+ extension.enableIntrinsicRemember.value(false)
+ }
+ }
+
+ private fun testComposeFeatureFlags(flags: List<String>, configure: (ComposeCompilerGradlePluginExtension) -> Unit) {
+ testComposeOptions({ extension, _ -> configure(extension) }) { options, _ ->
+ for (flag in flags) options.assertFeature(flag)
+ }
+ }
+
private fun testComposeOptions(
configureExtension: (ComposeCompilerGradlePluginExtension, Project) -> Unit = { _, _ -> },
assertions: (List<Pair<String, String>>, Project) -> Unit
@@ -116,4 +187,9 @@
}
private fun Project.simulateAgpPresence() = configurations.resolvable("kotlin-extension")
-}
\ No newline at end of file
+}
+
+fun List<Pair<String, String>>.assertFeature(featureName: String) =
+ assertTrue(
+ firstOrNull { it.first == "featureFlag" && it.second == featureName } != null,
+ "Expected a feature flag $featureName to be set in $this")
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ComposeIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ComposeIT.kt
index 1968246..c237497 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ComposeIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ComposeIT.kt
@@ -58,9 +58,6 @@
":compileDebugKotlin",
"-P plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaClasses=false," +
"plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=false," +
- "plugin:androidx.compose.compiler.plugins.kotlin:intrinsicRemember=true," +
- "plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=false," +
- "plugin:androidx.compose.compiler.plugins.kotlin:strongSkipping=false," +
"plugin:androidx.compose.compiler.plugins.kotlin:traceMarkersEnabled=false",
LogLevel.INFO
)