Skip to content

Commit b1e86ad

Browse files
authored
Generate initializer functions in the Res file to avoid the MethodTooLargeException (JetBrains#4205)
1 parent b4881ff commit b1e86ad

File tree

5 files changed

+290
-178
lines changed
  • gradle-plugins/compose/src

5 files changed

+290
-178
lines changed

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

+96-59
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package org.jetbrains.compose.resources
22

33
import com.squareup.kotlinpoet.*
44
import java.nio.file.Path
5+
import java.util.SortedMap
6+
import java.util.TreeMap
57
import kotlin.io.path.invariantSeparatorsPathString
68

79
internal enum class ResourceType(val typeName: String) {
@@ -26,12 +28,14 @@ internal data class ResourceItem(
2628
val path: Path
2729
)
2830

29-
private fun ResourceItem.getClassName(): ClassName = when (type) {
31+
private fun ResourceType.getClassName(): ClassName = when (this) {
3032
ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource")
3133
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
3234
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource")
3335
}
3436

37+
private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
38+
3539
private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder {
3640
val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier")
3741
val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier")
@@ -101,85 +105,118 @@ internal fun getResFileSpec(
101105
//type -> id -> items
102106
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
103107
packageName: String
104-
): FileSpec = FileSpec.builder(packageName, "Res").apply {
105-
addType(TypeSpec.objectBuilder("Res").apply {
106-
addModifiers(KModifier.INTERNAL)
108+
): FileSpec =
109+
FileSpec.builder(packageName, "Res").apply {
107110
addAnnotation(
108111
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
109112
.addMember("org.jetbrains.compose.resources.InternalResourceApi::class")
110-
.build()
111-
)
112-
addAnnotation(
113-
AnnotationSpec.builder(ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi"))
113+
.addMember("org.jetbrains.compose.resources.ExperimentalResourceApi::class")
114114
.build()
115115
)
116116

117-
//readFileBytes
118-
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
119-
addFunction(
120-
FunSpec.builder("readBytes")
121-
.addKdoc("""
117+
//we need to sort it to generate the same code on different platforms
118+
val sortedResources = sortResources(resources)
119+
120+
addType(TypeSpec.objectBuilder("Res").apply {
121+
addModifiers(KModifier.INTERNAL)
122+
addAnnotation(
123+
AnnotationSpec.builder(
124+
ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi")
125+
).build()
126+
)
127+
128+
//readFileBytes
129+
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
130+
addFunction(
131+
FunSpec.builder("readBytes")
132+
.addKdoc(
133+
"""
122134
Reads the content of the resource file at the specified path and returns it as a byte array.
123135
124136
Example: `val bytes = Res.readBytes("files/key.bin")`
125137
126138
@param path The path of the file to read in the compose resource's directory.
127139
@return The content of the file as a byte array.
128-
""".trimIndent())
129-
.addParameter("path", String::class)
130-
.addModifiers(KModifier.SUSPEND)
131-
.returns(ByteArray::class)
132-
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
133-
.build()
134-
)
140+
""".trimIndent()
141+
)
142+
.addParameter("path", String::class)
143+
.addModifiers(KModifier.SUSPEND)
144+
.returns(ByteArray::class)
145+
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
146+
.build()
147+
)
148+
val types = sortedResources.map { (type, idToResources) ->
149+
getResourceTypeObject(type, idToResources)
150+
}
151+
addTypes(types)
152+
}.build())
135153

136-
val types = resources.map { (type, idToResources) ->
137-
getResourceTypeObject(type, idToResources)
138-
}.sortedBy { it.name }
139-
addTypes(types)
140-
}.build())
141-
}.build()
154+
sortedResources
155+
.flatMap { (type, idToResources) ->
156+
idToResources.map { (name, items) ->
157+
getResourceInitializer(name, type, items)
158+
}
159+
}
160+
.forEach { addFunction(it) }
161+
}.build()
142162

143163
private fun getResourceTypeObject(type: ResourceType, nameToResources: Map<String, List<ResourceItem>>) =
144164
TypeSpec.objectBuilder(type.typeName).apply {
145-
nameToResources.entries
146-
.sortedBy { it.key }
147-
.forEach { (name, items) ->
148-
addResourceProperty(name, items.sortedBy { it.path })
165+
nameToResources.keys
166+
.forEach { name ->
167+
addProperty(
168+
PropertySpec
169+
.builder(name, type.getClassName())
170+
.initializer("get_$name()")
171+
.build()
172+
)
149173
}
150174
}.build()
151175

152-
private fun TypeSpec.Builder.addResourceProperty(name: String, items: List<ResourceItem>) {
153-
val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
154-
155-
val first = items.first()
156-
val propertyClassName = first.getClassName()
157-
val resourceId = first.let { "${it.type}:${it.name}" }
158-
159-
val initializer = CodeBlock.builder()
160-
.add("%T(\n", propertyClassName).withIndent {
161-
add("\"$resourceId\",\n")
162-
if (first.type == ResourceType.STRING) {
163-
add("\"${first.name}\",\n")
164-
}
165-
add("setOf(\n").withIndent {
166-
items.forEach { item ->
167-
add("%T(\n", resourceItemClass).withIndent {
168-
add("setOf(").addQualifiers(item).add("),\n")
169-
//file separator should be '/' on all platforms
170-
add("\"${item.path.invariantSeparatorsPathString}\"\n") //todo: add module ID here
176+
private fun getResourceInitializer(name: String, type: ResourceType, items: List<ResourceItem>): FunSpec {
177+
val propertyTypeName = type.getClassName()
178+
val resourceId = "${type}:${name}"
179+
return FunSpec.builder("get_$name")
180+
.addModifiers(KModifier.PRIVATE)
181+
.returns(propertyTypeName)
182+
.addStatement(
183+
CodeBlock.builder()
184+
.add("return %T(\n", propertyTypeName).withIndent {
185+
add("\"$resourceId\",")
186+
if (type == ResourceType.STRING) add(" \"$name\",")
187+
withIndent {
188+
add("\nsetOf(\n").withIndent {
189+
items.forEach { item ->
190+
add("%T(", resourceItemClass)
191+
add("setOf(").addQualifiers(item).add("), ")
192+
//file separator should be '/' on all platforms
193+
add("\"${item.path.invariantSeparatorsPathString}\"") //todo: add module ID here
194+
add("),\n")
195+
}
196+
}
197+
add(")\n")
171198
}
172-
add("),\n")
173199
}
174-
}
175-
add(")\n")
176-
}
177-
.add(")")
200+
.add(")")
201+
.build().toString()
202+
)
178203
.build()
204+
}
179205

180-
addProperty(
181-
PropertySpec.builder(name, propertyClassName)
182-
.initializer(initializer)
183-
.build()
184-
)
206+
private fun sortResources(
207+
resources: Map<ResourceType, Map<String, List<ResourceItem>>>
208+
): TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>> {
209+
val result = TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>>()
210+
resources
211+
.entries
212+
.forEach { (type, items) ->
213+
val typeResult = TreeMap<String, List<ResourceItem>>()
214+
items
215+
.entries
216+
.forEach { (name, resItems) ->
217+
typeResult[name] = resItems.sortedBy { it.path }
218+
}
219+
result[type] = typeResult
220+
}
221+
return result
185222
}

gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

+86-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlin.io.path.Path
99

1010
class ResourcesTest : GradlePluginTestBase() {
1111
@Test
12-
fun testGeneratedAccessorsAndCopiedFonts(): Unit = with(testProject("misc/commonResources")) {
12+
fun testGeneratedAccessors(): Unit = with(testProject("misc/commonResources")) {
1313
//check generated resource's accessors
1414
gradle("generateComposeResClass").checks {
1515
assertEqualTextFiles(
@@ -137,4 +137,89 @@ class ResourcesTest : GradlePluginTestBase() {
137137
}
138138
gradle("jar")
139139
}
140+
141+
//https://github.com/JetBrains/compose-multiplatform/issues/4194
142+
@Test
143+
fun testHugeNumberOfStrings(): Unit = with(
144+
//disable cache for the test because the generateStringFiles task doesn't support it
145+
testProject("misc/commonResources", defaultTestEnvironment.copy(useGradleConfigurationCache = false))
146+
) {
147+
file("build.gradle.kts").let { f ->
148+
val originText = f.readText()
149+
f.writeText(
150+
buildString {
151+
appendLine("import java.util.Locale")
152+
append(originText)
153+
appendLine()
154+
append("""
155+
val template = ""${'"'}
156+
<resources>
157+
<string name="app_name">Compose Resources App</string>
158+
<string name="hello">😊 Hello world!</string>
159+
<string name="multi_line">Lorem ipsum dolor sit amet,
160+
consectetur adipiscing elit.
161+
Donec eget turpis ac sem ultricies consequat.</string>
162+
<string name="str_template">Hello, %1${'$'}{"$"}s! You have %2${'$'}{"$"}d new messages.</string>
163+
<string-array name="str_arr">
164+
<item>item 1</item>
165+
<item>item 2</item>
166+
<item>item 3</item>
167+
</string-array>
168+
[ADDITIONAL_STRINGS]
169+
</resources>
170+
""${'"'}.trimIndent()
171+
172+
val generateStringFiles = tasks.register("generateStringFiles") {
173+
val numberOfLanguages = 20
174+
val numberOfStrings = 500
175+
val langs = Locale.getAvailableLocales()
176+
.map { it.language }
177+
.filter { it.count() == 2 }
178+
.sorted()
179+
.distinct()
180+
.take(numberOfLanguages)
181+
.toList()
182+
183+
val resourcesFolder = project.file("src/commonMain/composeResources")
184+
185+
doLast {
186+
// THIS REMOVES THE `values` FOLDER IN `composeResources`
187+
// THIS REMOVES THE `values` FOLDER IN `composeResources`
188+
// Necessary when reducing the number of languages.
189+
resourcesFolder.listFiles()?.filter { it.name.startsWith("values") }?.forEach {
190+
it.deleteRecursively()
191+
}
192+
193+
langs.forEachIndexed { langIndex, lang ->
194+
val additionalStrings =
195+
(0 until numberOfStrings).joinToString(System.lineSeparator()) { index ->
196+
""${'"'}
197+
<string name="string_${'$'}{index.toString().padStart(4, '0')}">String ${'$'}index in lang ${'$'}lang</string>
198+
""${'"'}.trimIndent()
199+
}
200+
201+
val langFile = if (langIndex == 0) {
202+
File(resourcesFolder, "values/strings.xml")
203+
} else {
204+
File(resourcesFolder, "values-${'$'}lang/strings.xml")
205+
}
206+
langFile.parentFile.mkdirs()
207+
langFile.writeText(template.replace("[ADDITIONAL_STRINGS]", additionalStrings))
208+
}
209+
}
210+
}
211+
212+
tasks.named("generateComposeResClass") {
213+
dependsOn(generateStringFiles)
214+
}
215+
""".trimIndent())
216+
}
217+
)
218+
}
219+
gradle("desktopJar").checks {
220+
check.taskSuccessful(":generateStringFiles")
221+
check.taskSuccessful(":generateComposeResClass")
222+
assert(file("src/commonMain/composeResources/values/strings.xml").readLines().size == 513)
223+
}
224+
}
140225
}

0 commit comments

Comments
 (0)