Repository navigation
feat(preview): support configurable Compose preview annotation - #41
Conversation
kingsword09
commented
Jun 4, 2026
- add previewAnnotationClass DSL option with AndroidX Preview as default
- normalize generated preview imports for modern Compose Multiplatform
- document Preview usage for Compose Multiplatform 1.10+ and legacy versions
- update example to Kotlin 2.3.21 and Compose Multiplatform 1.11.1
- switch example official icon source to GitHub raw to avoid rawcdn 403 failures
- refresh generated example icons
- add previewAnnotationClass DSL option with AndroidX Preview as default - normalize generated preview imports for modern Compose Multiplatform - document Preview usage for Compose Multiplatform 1.10+ and legacy versions - update example to Kotlin 2.3.21 and Compose Multiplatform 1.11.1 - switch example official icon source to GitHub raw to avoid rawcdn 403 failures - refresh generated example icons
There was a problem hiding this comment.
Code Review
This pull request introduces a new previewAnnotationClass configuration option to SymbolCraft, allowing users to customize the fully qualified class name of the @Preview annotation used in generated icon preview functions, which now defaults to androidx.compose.ui.tooling.preview.Preview. It updates the converter, Gradle plugin extension, and example app to support this feature, alongside upgrading Kotlin and Compose Multiplatform versions in the example. The code reviewer suggested a performance optimization in Svg2ComposeConverter.kt to replace multiple full-string splits and joins with a single-pass line-by-line processing loop, preventing build overhead on large icon libraries.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private fun normalizePreviewAnnotation( | ||
| content: String, | ||
| generatePreview: Boolean, | ||
| previewAnnotationClass: String, | ||
| ): String { | ||
| if (!generatePreview || !content.contains("@Preview")) return content | ||
|
|
||
| val annotationClass = previewAnnotationClass.trim() | ||
| require(annotationClass.matches(KOTLIN_QUALIFIED_NAME_REGEX)) { | ||
| "previewAnnotationClass must be a fully qualified Kotlin annotation class name. " + | ||
| "Got: '$previewAnnotationClass'" | ||
| } | ||
|
|
||
| val annotationName = annotationClass.substringAfterLast('.') | ||
| val withoutPreviewImports = | ||
| content | ||
| .lines() | ||
| .filterNot { line -> | ||
| val importedClass = line.removePrefix("import ").trim() | ||
| line.startsWith("import ") && | ||
| (importedClass in GENERATED_PREVIEW_IMPORTS || | ||
| importedClass == annotationClass) | ||
| } | ||
| .joinToString("\n") | ||
|
|
||
| val withAnnotationName = | ||
| withoutPreviewImports.replace(PREVIEW_ANNOTATION_REGEX, "@$annotationName") | ||
|
|
||
| return addImportIfMissing(withAnnotationName, annotationClass) | ||
| } | ||
|
|
||
| /** Adds an import after the existing import block, or immediately after the package line. */ | ||
| private fun addImportIfMissing(content: String, qualifiedName: String): String { | ||
| if (content.lines().any { it == "import $qualifiedName" }) return content | ||
|
|
||
| val lines = content.lines().toMutableList() | ||
| val lastImportIndex = lines.indexOfLast { it.startsWith("import ") } | ||
| val packageLineIndex = lines.indexOfFirst { it.startsWith("package ") } | ||
| val insertIndex = | ||
| when { | ||
| lastImportIndex >= 0 -> lastImportIndex + 1 | ||
| packageLineIndex >= 0 -> packageLineIndex + 1 | ||
| else -> 0 | ||
| } | ||
|
|
||
| lines.add(insertIndex, "import $qualifiedName") | ||
| return lines.joinToString("\n") | ||
| } |
There was a problem hiding this comment.
The current implementation of normalizePreviewAnnotation and addImportIfMissing performs multiple full-string splits (content.lines()) and joins (joinToString("\n")) on every generated file. Since this post-processing runs for every generated icon, this overhead can significantly slow down build times for large icon libraries.
We can optimize this into a single-pass line-by-line processing loop that filters imports, replaces the annotation, and inserts the new import at the correct position in one go.
private fun normalizePreviewAnnotation(
content: String,
generatePreview: Boolean,
previewAnnotationClass: String,
): String {
if (!generatePreview || !content.contains("@Preview")) return content
val annotationClass = previewAnnotationClass.trim()
require(annotationClass.matches(KOTLIN_QUALIFIED_NAME_REGEX)) {
"previewAnnotationClass must be a fully qualified Kotlin annotation class name. " +
"Got: '$previewAnnotationClass'"
}
val annotationName = annotationClass.substringAfterLast('.')
val lines = content.lines()
val filteredLines = ArrayList<String>(lines.size + 1)
var lastImportIndex = -1
var packageLineIndex = -1
for (line in lines) {
if (line.startsWith("import ")) {
val importedClass = line.removePrefix("import ").trim()
if (importedClass in GENERATED_PREVIEW_IMPORTS || importedClass == annotationClass) {
continue
}
filteredLines.add(line)
lastImportIndex = filteredLines.size - 1
} else {
if (line.startsWith("package ")) {
packageLineIndex = filteredLines.size
}
val replacedLine = if (line.contains("@Preview")) {
line.replace(PREVIEW_ANNOTATION_REGEX, "@$annotationName")
} else {
line
}
filteredLines.add(replacedLine)
}
}
val insertIndex = when {
lastImportIndex >= 0 -> lastImportIndex + 1
packageLineIndex >= 0 -> packageLineIndex + 1
else -> 0
}
filteredLines.add(insertIndex, "import $annotationClass")
return filteredLines.joinToString("\\n")
}