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 任务。

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 图标)
  1. 下载图标库:打开 Google Fonts Icons,选一个图标,切到 “Android” 标签,点击下载(会得到一个 XML 文件)。​
  2. 改图标文件:用记事本打开 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 专属写法)。

  1. 放对位置:把改好的 XML 文件放进 composeResources/drawable 文件夹。​
  2. 在代码里用:和普通图片一样用 painterResource (),还能改颜色:
Image(
    painter = painterResource(Res.drawable.图标文件名),  // 图标文件名就是XML的文件名
    contentDescription = "示例图标",  // 无障碍描述
    modifier = Modifier.size(24.dp),  // 图标大小
    colorFilter = ColorFilter.tint(Color.Blue)  // 把图标改成蓝色
)
3. 文字资源(values 文件夹里的字符串)

文字都存在 values 文件夹的 XML 文件里,支持普通文字、带变量的文字、文字数组、复数形式四种场景。

  1. 普通文字,先在 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 那样转义 @ 或 ?(直接写就行)。
  1. 带变量的文字(文字模板)
    比如想显示 “你有 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 这种小数)。

  1. 文字数组
    想把一组相关文字存在一起(比如下拉菜单选项),先在 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])
  1. 复数文字(根据数量变格式)
<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. 特殊场景处理
  1. 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。

  1. 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.我的矢量图)
  1. 调用外部库处理多平台资源
    如果想让外部库(比如视频播放库)处理多平台资源,先调用 Res.getUri() 获取资源的 “平台专属路径”,再传给外部库:
// 获取 files/我的视频.mp4 这个文件的路径
val videoPath = Res.getUri("files/我的视频.mp4")

// 把路径传给外部视频库(比如ExoPlayer)
videoPlayer.loadVideo(videoPath)
  1. 远程文件(从网上加载的文件)​
    多平台资源库只管 “应用里自带的资源”,如果要加载网上的文件(比如从服务器拉图片),需要用专门的库:​
    图片加载:Compose ImageLoader、Kamel;​
    网络请求:Ktor Client(先下载文件,再处理)。​

  2. 关于 Java 资源​
    虽然 Compose Multiplatform 也能用水印的 Java 资源,但不推荐 ——Java 资源没有 “自动生成访问入口”“多模块支持”“本地化” 这些功能,建议尽量用多平台资源库。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐