commonMain couldn't touch resources at all. Not strings, not drawables. Reading one string in shared code meant a hand-written expect/actual pair for every platform, and each drawable needed its own loader on each target.
#The old way
expect class StringProvider {
fun welcomeMessage(): String
}
actual class StringProvider(private val context: Context) {
actual fun welcomeMessage(): String = context.getString(R.string.welcome_message)
}
#The new way
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import myapp.composeapp.generated.resources.Res
import myapp.composeapp.generated.resources.welcome_message
import myapp.composeapp.generated.resources.ic_logo
@Composable
fun WelcomeScreen() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = painterResource(Res.drawable.ic_logo),
contentDescription = null
)
Text(text = stringResource(Res.string.welcome_message))
}
}
#Why it matters
- Gradle generates
Resat build time fromcomposeResources/drawableandcomposeResources/values/strings.xmlin the shared module. One file tree. Every target reads from it. stringResourceandpainterResourceare plain composables. Call them straight fromcommonMain, with noexpect/actualboilerplate per resource.- Qualifier folders for dark, night, and locale behave exactly as they do in Android resources, resolved per platform at runtime.
#Gotcha
Res is generated code, tied to the module that declared the resources.
Reference Res.string.welcome_message before Gradle's resource-generation task has run — a fresh clone, or a resource added but not yet synced — and the whole module fails to compile. The error says "unresolved reference". It reads like a typo, not a missing resource.