Kotlin Multiplatform 图片、字符串、字体资源共用代码资源访问
KMP 提供了专门的 compose-multiplatform-resources 库和 Gradle 插件,可在所有支持的平台上通过通用代码来访问资源。资源包含图片、字体、字符串等静态内容,可直接在应用中使用。
使用资源时,需要注意以下几点:
- 几乎所有资源都是在调用线程中同步读取的。只有原始文件和网络资源是异步读取的,这是仅有的例外情况。
- 目前还不支持以流的形式读取大型原始文件(比如长视频)。这种情况下,可以使用 getUri () 函数将文件单独传递给系统 API(例如 kotlinx-io 库)。
- 从 1.6.10 版本开始,只要你使用的是 Kotlin 2.0.0 或更高版本,以及 Gradle 7.6 或更高版本,就可以把资源放在任何模块或源集中。
添加依赖项
要在多平台项目中访问资源,需添加库依赖并在项目目录中组织文件结构。在 composeApp 目录下的 build.gradle.kts 文件中,向 commonMain 源集添加依赖,注意新版本已默认集成。
项目地址:https://central.sonatype.com/artifact/org.jetbrains.compose.components/components-resources
kotlin {
//...
sourceSets {
commonMain.dependencies {
implementation(compose.components.resources)
}
}
}
配置资源目录
添加 composeResources 目录用于存放资源文件,请根据以下规则组织目录结构:
- drawable:图像资源文件,支持光栅化图片(JPEG、PNG、位图和 WebP)和矢量 XML 图片。
- font:字体文件。
- values:字符串(strings.xml)。
- files:其他任何层次结构的文件。

自定义资源目录几种方式
在 build.gradle.kts 文件的 compose.resources {} 块中,你可以为每个源集指定自定义资源目录。这些自定义目录中的文件组织方式应与默认的 composeResources 相同:图像放在 drawable 子目录,字体放在 font 子目录,依此类推。
1. 指向特定文件夹
compose.resources {
customDirectory(
sourceSetName = "jvmMain",
directoryProvider = provider { layout.projectDirectory.dir("desktopResources") }
)
}
2. 通过类指定路径
还可以在 Gradle 中通过指定类的方式来实现。
abstract class DownloadRemoteFiles : DefaultTask() {
@get:OutputDirectory
val outputDir = layout.buildDirectory.dir("downloadedRemoteFiles")
@TaskAction
fun run() { /* your code for downloading files */ }
}
compose.resources {
customDirectory(
sourceSetName = "iosMain",
directoryProvider = tasks.register<DownloadRemoteFiles>("downloadedRemoteFiles").map { it.outputDir.get() }
)
}
3. 自定义网络资源路径
可以使用 configureWebResources () 函数指定网络资源的路径和 URL:
- 使用相对路径(以 / 开头)引用来自域根的资源。
- 使用绝对 URL(以 http:// 或 https:// 开头)引用托管在外部域或 CDN 上的资源。
// Maps resources to an application-specific path
configureWebResources {
resourcePathMapping { path -> "/myApp/resources/$path" }
}
// Maps resources to an external CDN
configureWebResources {
resourcePathMapping { path -> "https://mycdn.com/myApp/res/$path" }
}
限定符
同一资源应根据环境(例如区域设置、屏幕密度或界面主题)以不同的方式呈现。例如,您可能需要本地化不同语言的文本或调整深色主题的图像。为此,库提供了特殊的限定符。
除了 files 目录外所有资源类型都支持限定符。使用 ”-“ 将限定符添加到目录名称中:

支持(按优先级顺序)以下限定符:语言、主题和密度。不同类型的限定符可以一起使用。例如,“drawable-en-rUS-mdpi-dark” 表示适用于美国地区的英语、160 DPI 屏幕和深色主题的图像。如果具有请求的限定符的资源不可访问,则会使用默认资源。
1. 语言和区域限定符
可以组合使用语言和区域限定符
- 语言由两个字母或三个字母的语言代码定义。
- 你可以在语言代码后添加两个字母的区域代码。区域代码必须带有小写的 r 前缀,例如:drawable-spa-rMX
2. 主题限定符
你可以添加 “light”(浅色)或 “dark”(深色)限定符。Compose Multiplatform 会根据当前系统主题选择必要的资源。
3. 密度限定符
你可以使用以下密度限定符,资源会根据系统中定义的屏幕密度进行选择。
- “ldpi” – 120 DPI,0.75 倍密度
- “mdpi” – 160 DPI,1 倍密度
- “hdpi” – 240 DPI,1.5 倍密度
- “xhdpi” – 320 DPI,2 倍密度
- “xxhdpi” – 480 DPI,3 倍密度
- “xxxhdpi” – 640 DPI,4 倍密度
使用资源文件
配置好资源后,一定要先构建项目!构建后会自动生成一个 “资源访问类”(默认叫 Res),后续所有资源都靠这个类调用。如果后续改了资源,重新构建项目就能更新这个类。当然你也可以手动运行 Gradle 中的 generateComposeResClass 任务。

导入自动生成的资源类
import 项目名.composeapp.generated.resources.Res
import 项目名.composeapp.generated.resources.示例图片名
这里的 “项目名” 是你自己的项目名称,“composeapp” 是放资源的模块名,“示例图片名” 是你放在 drawable 里的图片文件名(比如 example_image)。
自定义资源访问类
默认生成的 Res 类可能不符合你的需求(比如想改包名、改访问权限),可以在 build.gradle.kts 文件里改配置。配置写在 compose.resources {} 块里,举个常用配置的例子:
compose.resources {
publicResClass = false // Res类是否公开:true=所有模块能用,false=仅当前模块能用(默认)
packageOfResClass = "me.sample.library.resources" // 给Res类指定包名(默认包名是“项目组名.模块名.generated.resources”)
generateResClass = auto // 何时生成Res类:auto=有资源依赖时自动生成(默认),always=强制生成
}
各种资源使用
1. 图片资源(drawable 文件夹里的图)
不管是普通图片(PNG/JPG 等)还是 Android 矢量图(XML),都能用下面的方法调用,只是函数不一样:
| 类型 | 函数 | 返回类型 | 适用场景 |
|---|---|---|---|
| 普通加载图片 | painterResource() | Painter | 大多数界面显示图片的场景 |
| 加载为位图(像素级) | imageResource() | ImageBitmap | 需要处理像素的场景(如裁剪) |
| 加载为矢量图 | vectorResource() | ImageVector | 需要缩放不失真的场景 |
注意:SVG 图片除了 Android 平台,其他平台(iOS / 桌面 / web)都支持。
举个例子:界面上显示一张图
Image(
painter = painterResource(Res.drawable.我的图片名), // 调用Res里的图片
contentDescription = null // 图片描述(无障碍用,不需要可以写null)
)
2. 图标资源(Material Symbols 图标)
- 下载图标库:打开 Google Fonts Icons,选一个图标,切到 “Android” 标签,点击下载(会得到一个 XML 文件)。
- 改图标文件:用记事本打开 XML,做两个修改:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="..."/> <!-- 这里是图标形状数据,不用改 -->
</vector>
把 android:tint 这行删掉(避免颜色冲突);
把 android:fillColor 的值改成具体颜色(比如 #000000 代表黑色,别用 @android:color/white 这种 Android 专属写法)。
- 放对位置:把改好的 XML 文件放进 composeResources/drawable 文件夹。
- 在代码里用:和普通图片一样用 painterResource (),还能改颜色:
Image(
painter = painterResource(Res.drawable.图标文件名), // 图标文件名就是XML的文件名
contentDescription = "示例图标", // 无障碍描述
modifier = Modifier.size(24.dp), // 图标大小
colorFilter = ColorFilter.tint(Color.Blue) // 把图标改成蓝色
)
3. 文字资源(values 文件夹里的字符串)
文字都存在 values 文件夹的 XML 文件里,支持普通文字、带变量的文字、文字数组、复数形式四种场景。
- 普通文字,先在 XML 里定义:
<resources>
<string name="app_name">我的超赞应用</string> <!-- name是调用时的key,内容是显示的文字 -->
<string name="title">首页标题</string>
</resources>
再在代码里调用(分 “组件内” 和 “组件外” 两种场景):
- 组件内(比如 Text 组件里):
Text(stringResource(Res.string.app_name)) // 直接显示“我的超赞应用”
- 组件外(比如逻辑代码里):需要用 LocalContext.current 辅助,具体查官方文档(日常开发用组件内的场景最多)
小技巧:文字里可以加特殊符号:
- \n 代表换行,\t 代表缩进;
- \uXXXX 代表特殊字符(比如 \u2605 是五角星);
- 不用像 Android 那样转义 @ 或 ?(直接写就行)。
- 带变量的文字(文字模板)
比如想显示 “你有 100 条新消息”,数字 100 是变量,先在 XML 里定义模板:
<resources>
<!-- %2$s 代表第二个字符串变量,%1$d 代表第一个数字变量 -->
<string name="str_template">你好,%2$s!你有 %1$d 条新消息。</string>
</resources>
再在代码里传变量(变量顺序要和模板里的数字对应):
// 100 对应 %1$d,"小明" 对应 %2$s,最终显示“你好,小明!你有 100 条新消息。”
Text(stringResource(Res.string.str_template, 100, "小明"))
注意:模板里的 $d(数字)和 $s(字符串)可以混用,甚至数字也能用 s (比如 s(比如 %1 s(比如s 也能传 100.1f 这种小数)。
- 文字数组
想把一组相关文字存在一起(比如下拉菜单选项),先在 XML 里定义数组:
<resources>
<string-array name="menu_options"> <!-- name是数组的key -->
<item>选项1 \u2605</item> <!-- 每个item是数组里的元素,支持特殊符号 -->
<item>选项2 \u2318</item>
<item>选项3 \u00BD</item>
</string-array>
</resources>
再在代码里调用(会返回一个 List):
// 获取数组
val menuList = stringArrayResource(Res.array.menu_options)
// 显示第一个元素(如果数组不为空)
if (menuList.isNotEmpty()) Text(menuList[0])
- 复数文字(根据数量变格式)
<resources>
<plurals name="new_message"> <!-- name是复数组的key -->
<item quantity="one">%1$d 条新消息</item> <!-- 数量为1时显示这个 -->
<item quantity="other">%1$d 条新消息</item> <!-- 数量不为1时显示这个 -->
</plurals>
</resources>
再在代码里调用(需要传 “数量” 参数):
// 数量为1时显示“1 条新消息”,数量为5时显示“5 条新消息”
Text(pluralStringResource(Res.plurals.new_message, 数量, 数量))
注意:除了 one(数量 1)和 other(其他数量),还支持 zero(数量 0)、two(数量 2)等,但不是所有语言都能用(比如英语不区分 zero,会按 other 处理),最好让懂对应语言的人确认规则。
4. 字体资源(font 文件夹里的字体文件)
把自定义字体(TTF/OTF 格式)放进 composeResources/font 文件夹,然后在代码里加载成字体样式:
@Composable
private fun MyCustomFontStyle(): Typography {
// 加载字体文件(Res.font.字体文件名 就是font文件夹里的文件名)
val customFont = Font(
resource = Res.font.我的字体名,
weight = FontWeight.Normal, // 字体粗细(正常/加粗等)
style = FontStyle.Normal // 字体样式(正常/斜体等)
)
// 把字体组合成字体家族,后续给Text组件用
val customFontFamily = FontFamily(customFont)
// 返回自定义的文字样式
return Typography(
bodyLarge = TextStyle(
fontFamily = customFontFamily,
fontSize = 16.sp
)
)
}
要在 Web 目标中支持表情符号或阿拉伯文字等特殊字符,您需要将相应的字体添加到资源并预加载回退字体。
5. 原始文件(files 文件夹里的任意文件)
比如音频、视频、文档等,放在 composeResources/files 文件夹里(里面可以建子文件夹分类),用 Res.readBytes() 读取成字节数组:
@Composable
fun ReadRawFile() {
// 用状态存字节数组(初始是空的)
var fileBytes by remember { mutableStateOf(ByteArray(0)) }
// 异步读取文件(避免卡界面)
LaunchedEffect(Unit) {
// 读取 files/我的文件夹/我的文件.bin 这个文件
fileBytes = Res.readBytes("files/我的文件夹/我的文件.bin")
}
// 把字节数组转成文字显示(如果是文本文件的话)
Text(fileBytes.decodeToString())
}
如果原始文件是图片(JPG/PNG/SVG 等)
可以把字节数组转成图片显示:
// 1. 字节数组转位图(比如PNG/JPG)
Image(fileBytes.decodeToImageBitmap(), contentDescription = null)
// 2. 字节数组转矢量图(比如XML矢量图)
Image(
imageVector = fileBytes.decodeToImageVector(LocalDensity.current),
contentDescription = null
)
// 3. 字节数组转SVG图(除了Android,其他平台都支持)
Image(
painter = fileBytes.decodeToSvgPainter(LocalDensity.current),
contentDescription = null
)
6. 用 “资源映射表” 快速访问资源
如果不知道具体资源名,或者想批量处理资源,可以用自动生成的 “资源映射表”—— 它把同类型资源做成了 Map(键是资源名,值是资源本身):
// 所有图片资源的映射表:key=图片名,value=图片资源
val allImages = Res.allDrawableResources
// 所有文字资源的映射表:key=文字名,value=文字资源
val allStrings = Res.allStringResources
// 举个例子:用映射表找“compose_multiplatform”这个图片并显示
Image(
painter = painterResource(allImages["compose_multiplatform"]!!),
contentDescription = null
)
6. 特殊场景处理
- Android 平台:多平台资源当 “资产文件” 用
从 Compose Multiplatform 1.7.0 开始,多平台资源会自动打包成 Android 的 “资产文件”(Assets),带来两个好处:
- Android Studio 能预览多平台组件(之前预览不了);
- 能直接给 WebView(网页视图)或媒体播放器传资源路径,比如用 Res.getUri(“files/index.html”) 获取 HTML 文件路径,给 WebView 加载。
注意:预览功能需要用新版 AGP(Android Gradle 插件):8.5.2、8.6.0-rc01 或 8.7.0-alpha04。
- Web端:提前加载资源避免卡顿
Web端的资源(字体、图片)是异步加载的,网络慢时可能出现 “文字先显示默认字体,加载完再变自定义字体”“图片先空白再显示” 的情况。可以用两种方法提前加载:
方法 1:用浏览器自带的预加载,先构建 Web 端项目:执行 ./gradlew :composeApp:wasmJsBrowserDistribution,会生成 dist 文件夹;在 dist 文件夹里找到要预加载的资源(比如字体文件),记下图路径;打开 dist 里的 index.html 文件,在 标签里加一行 标签:
<!-- href 是资源路径,type 是资源类型(字体用 font/ttf) -->
<link rel="preload"
href="./composeResources/项目名.composeapp.generated.resources/font/我的字体.ttf"
as="fetch"
type="font/ttf"
crossorigin/>
方法 2:用 Compose 自带的预加载 API(1.8.0+ 实验性),1.8.0 版本新增了预加载函数,支持字体、图片:
// 预加载字体
fontFamilyResolver.preload(FontFamily(Res.font.我的字体))
// 预加载图片
preloadImageBitmap(Res.drawable.我的图片)
// 预加载矢量图
preloadImageVector(Res.drawable.我的矢量图)
- 调用外部库处理多平台资源
如果想让外部库(比如视频播放库)处理多平台资源,先调用 Res.getUri() 获取资源的 “平台专属路径”,再传给外部库:
// 获取 files/我的视频.mp4 这个文件的路径
val videoPath = Res.getUri("files/我的视频.mp4")
// 把路径传给外部视频库(比如ExoPlayer)
videoPlayer.loadVideo(videoPath)
-
远程文件(从网上加载的文件)
多平台资源库只管 “应用里自带的资源”,如果要加载网上的文件(比如从服务器拉图片),需要用专门的库:
图片加载:Compose ImageLoader、Kamel;
网络请求:Ktor Client(先下载文件,再处理)。 -
关于 Java 资源
虽然 Compose Multiplatform 也能用水印的 Java 资源,但不推荐 ——Java 资源没有 “自动生成访问入口”“多模块支持”“本地化” 这些功能,建议尽量用多平台资源库。
更多推荐

所有评论(0)