compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

Generated res class

Open guillermolc opened this issue 1 year ago • 3 comments

Hi guys, im trying to use the new feature of resources but i want to access to the resources of some common lib on another but... the generated class is declerated as internal and i cant have access to that resources

can you check this use case? image

guillermolc avatar Feb 17 '24 22:02 guillermolc

Thanks for an Issue!

dima-avdeev-jb avatar Feb 19 '24 10:02 dima-avdeev-jb

Hi! It is by design. To access resources from another module you need to export accessors from the library where resources are introduced.

pjBooms avatar Feb 19 '24 11:02 pjBooms

Interesting @pjBooms but what is the correct way to do that?

guillermolc avatar Feb 19 '24 13:02 guillermolc

@pjBooms you mean something like this?

@OptIn(ExperimentalResourceApi::class)
object PublicRes {
    object drawable {
        val my_awesome_icon = Res.drawable.my_awesome_icon
    }
}

But what are the benefits of using Compose Resources library if I have to manually describe each resource? My usecase - reusable library of compose elements (single design system for all applications).

m0rtis avatar Feb 20 '24 16:02 m0rtis

Maybe an option to control the visibility of the generated Res class?

Extracting all resources in one common lib for others is a good practice, IMO.

xiaozhikang0916 avatar Feb 22 '24 03:02 xiaozhikang0916

As a temporary solution, you can add in the build.gradle.kts of "resource" module, the following code:

// After executing the "generateComposeResClass" task, we replace the internal stuff by public.
tasks.named("generateComposeResClass") {
    doLast {
        val dirName = buildString {
            val group = project.group.toString()
                .lowercase()
                .replace('.', '/')
                .replace('-', '_')
            append(group)
            if (group.isNotEmpty()) {
                append("/")
            }
            append(project.name.lowercase())
        }
        val dir = project.layout.buildDirectory
            .dir("generated/compose/resourceGenerator/kotlin/$dirName/generated/resources")
            .get()
            .asFile
        File(dir, "Res.kt").also {
            if (!it.exists()) {
                return@also
            }
            val content = it.readText()
            val updatedContent = content.replace("internal object Res {", "object Res {")
            it.writeText(updatedContent)
        }
        listOf("Drawable0.kt", "String0.kt").forEach { filename ->
            File(dir, filename).also { file ->
                if (!file.exists()) {
                    return@also
                }
                val content = file.readText()
                val updatedContent = content.replace("internal val Res", "val Res")
                file.writeText(updatedContent)
            }
        }
    }
}

After that, you can access to "Res" class of "resource" module in the others compose modules.

jfyoteau avatar Feb 23 '24 06:02 jfyoteau

@OptIn(ExperimentalResourceApi::class)
object PublicRes {
    object drawable {
        val my_awesome_icon = Res.drawable.my_awesome_icon
    }
}

no. it doesn't work because multimodule projects are not supported. The recommended way to do that is

class ModuleRes {
  fun getIconPainter() = painterResource(Res.module.icon)
}

but againt it won't work until we support multimodule projects

terrakok avatar Feb 26 '24 12:02 terrakok

I see a lot of confusion about the internal-ness of the Res class.

⚠️ First of all, it is required at the moment because the resources don't work in multimodule projects. ⚠️

But I'll try to explain an initial idea behind that:

  • The Res class is a container of static accessors for module's files. It reflects a file hierarchy by fact and it will be changed on each change in files.
  • It is a bad idea to make a public API of a module (another words an API of library) depending on the autogenerated code.
  • If you want to make a library provide some resources as its API then declare it explicitly. like
@Composable
fun getMyLibraryIconPainter(): Painter = painterResource(ID)

It guaranties a stability of the API.

Yes, it is not convenient for design-system or icon-pack libraries. I would generate the proposed code by the kotlin poet in my project for that :)

Since there is a huge request to add the feature to the gradle plugin. I added the feature to my backlog and will investigate pros and cons and there is a chance to have it in the future releases.

terrakok avatar Feb 26 '24 15:02 terrakok

The main issue is almost all companies use something like Lokalise, and that means having only one module for resources and the rest of the modules depend on it.

The use case of generating a public API would be interesting for a lot of people.

JavierSegoviaCordoba avatar Feb 26 '24 16:02 JavierSegoviaCordoba

To clarify why it is not a trivial task to change the class modifier only: because resources are packed different way on each target. So, to support the correct work with multimodule projects we have to be sure that the runtime may select correct path to read.

terrakok avatar Feb 26 '24 16:02 terrakok

It is a bad idea to make a public API of a module (another words an API of library) depending on the autogenerated code.

@terrakok I'm sorry, but with all my great respect for you, I can't agree with that. Suppose I have a library in which a gradle plugin generates some code. I really use such solutions in my projects. For example, I wrote an api client for YouTrack, in which files are generated during assembly based on the openapi description of the YouTrack API. These generated files are baked into the build artifact and shipped with the library. Of course, the YouTrack API may change in the future. In this case, I will simply update the version of my library and publish this version with newly generated files corresponding to the updated API.

Another example is compose web, which generates js and wasm files in one module of my application (composeApp). And I configured gradle so that the files generated by the compose plugin are copied to the build-folder of another module (server). And the ktor server serves these generated files as static resources.

Something similar would be cool to implement with compose resources - during the build of the library with resources, the resource files are baked into the final artifact (jar) and accessible from the application using the library. And if I change something in the icon library, I'll just publish a new version of this library. And an application that uses the previous version will only see the previous version of the icons. And if you update the version, new versions of the icons will become available.

m0rtis avatar Mar 16 '24 10:03 m0rtis

https://github.com/JetBrains/compose-multiplatform/pull/4482

terrakok avatar Mar 19 '24 21:03 terrakok

The main issue is almost all companies use something like Lokalise, and that means having only one module for resources and the rest of the modules depend on it.

Yikes, I didn't realise that Lokalise had this limitation of only one module having resources. That sort of thing definitely goes into the list of things to consider when choosing a translation vendor.

hakanai avatar Apr 08 '24 02:04 hakanai