首页 / 资讯中心 / 文章详情

【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题

【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题 ★ FEATURED ARTICLE
本课目标掌握composeResources的多平台资源目录结构与类型安全访问器理解限定符的优先级与本地化目录命名规则特别是中文双脚本回退机制掌握 Coil 3 在 CMP 中的网络图片加载与 iOS 内存压力适配建立自定义主题、扩展颜色与深色模式的系统适配方法。系列整体规划课次主题核心内容难度第1课从零开始技术概览、环境搭建、第一个应用、代码解读⭐第2课Compose 基础语法Composable、状态管理、重组机制、Modifier 体系⭐⭐第3课布局与组件Column/Row/Box、LazyColumn、Material3 组件库⭐⭐第4课导航与路由Navigation Compose、类型安全路由、深层链接⭐⭐⭐第5课网络与数据层Ktor 客户端、序列化、Repository 模式⭐⭐⭐第6课状态管理与架构ViewModel、单向数据流、依赖注入⭐⭐⭐⭐第7课平台适配与互操作expect/actual、SwiftUI 互操作、平台特定 API⭐⭐⭐⭐第8课资源管理与主题多平台资源、图片加载、深浅色主题⭐⭐⭐第9课测试与调试Compose UI 测试、单元测试、性能分析⭐⭐⭐⭐第10课发布与部署Android/iOS/桌面/Web 打包发布、CI/CD⭐⭐⭐⭐⭐第8课 资源管理与主题一、Compose Multiplatform 资源系统1.1 从“平台各管各的”到“统一资源目录”Android 原生开发用res/目录和R.drawable.xxxiOS 用 Asset Catalog 和 Localizable.strings桌面端又有自己的资源加载方式。每个平台的资源系统各不相同跨平台共享资源一直是痛点。Compose Multiplatform 1.6.0 引入的统一资源 API 从根本上解决了这个问题。你只需要在commonMain下创建一个composeResources目录按约定结构组织文件构建时 CMP 会为所有平台生成统一的类型安全访问器。资源文件本身是平台无关的一张 PNG 在所有平台上都是同样的字节引用方式也是平台无关的Res.drawable.logo在所有平台上写法一致。平台差异被资源系统完全封装了。1.2 目录结构与四类资源在composeApp/src/commonMain/下创建composeResources目录按资源类型组织commonMain/composeResources/ ├── drawable/ # 图像资源 ├── font/ # 字体文件.ttf ├── values/ # 字符串资源strings.xml └── files/ # 其他任意文件四类资源的定位drawable存放所有图像。支持光栅格式PNG、JPEG、WebP和矢量 Android XML不引用 Android 资源。font存放.ttf字体文件。文件名建议使用小写字母、数字和下划线。values存放strings.xml字符串资源。这是本地化的核心目录。files存放其他任意文件JSON、二进制数据等。注意files目录不生成类型安全访问器需要通过Res.readBytes(files/xxx)手动读取且不支持限定符。1.3 生成类型安全访问器添加资源后构建项目CMP 会生成Res对象importproject.shared.generated.resources.Resimportproject.shared.generated.resources.logoComposablefunApp(){Image(painterpainterResource(Res.drawable.logo),contentDescriptionnull)Text(stringResource(Res.string.app_name))Text(自定义字体,fontFamilyFontFamily(Font(Res.font.roboto)))}painterResource用于图像stringResource用于字符串Font构造函数接受字体资源。如果Res无法解析检查三件事资源确实在composeResources下、build.gradle.kts中有implementation(compose.components.resources)、Gradle 已同步。1.4 限定符与本地化同一个资源在不同环境下可能需要不同版本。限定符通过目录名后缀表达用连字符连接。支持三类限定符按优先级排序语言 主题 密度。可组合使用如drawable-en-rUS-mdpi-dark表示美国英语、160 DPI、深色主题下的图像。语言限定符语言代码用双字母ISO 639-1或三字母ISO 639-2。区域代码带小写r前缀如drawable-spa-rMX。语言和区域代码区分大小写。主题限定符light或darkCMP 根据系统主题自动选择。密度限定符ldpi(120 DPI) 到xxxhdpi(640 DPI)。中文双脚本的回退陷阱在 Android 和桌面端系统可能请求不带书写方式的语言区域如zh而非zh-Hans。如果你只提供values-bzhHans和values-bzhHant所有请求都会匹配到它们导致资源无法解析而崩溃。必须同时提供values-zh作为无脚本回退commonMain/composeResources/ ├── values/ # 默认资源 ├── values-zh/ # 不带脚本的 zh 资源关键 ├── values-bzhHans/ # 简体中文 └── values-bzhHant/ # 繁体中文这个规则同样适用于塞尔维亚语sr-Cyrl、sr-Latn等具有多种脚本的语言。1.5 字符串模板与复数字符串模板支持%1$s和%2$d占位符$s和$d后缀无功能区别stringnamestr_templateHello, %2$s! You have %1$d new messages./stringText(stringResource(Res.string.str_template,100,User_name))复数资源支持zero、one、two、few、many、other六种量词。注意并非所有语言都使用所有量词英语只关心one和otherpluralsnamenew_messageitemquantityone%1$d new message/itemitemquantityother%1$d new messages/item/pluralsText(pluralStringResource(Res.plurals.new_message,1,1))二、网络图片加载Coil 32.1 为什么用 Coil 而不是手写painterResource只能加载打包在应用内的静态资源。网络图片需要异步下载、缓存、解码、显示并处理占位图和错误状态。Coil 3 原生支持 Compose Multiplatform核心优势是内存和磁盘缓存、图片降采样、自动暂停/取消请求。2.2 配置与引擎选择在commonMain中添加依赖implementation(io.coil-kt.coil3:coil-compose:3.5.0)implementation(io.coil-kt.coil3:coil-network-ktor3:3.5.0)关键配置CMP 项目必须用 Ktor 引擎而不是 OkHttp。Coil 官方文档明确指出使用 Compose Multiplatform 时需要 Ktor 而非 OkHttp因为 OkHttp 只在 Android 和 JVM 上可用。2.3 基本使用与全局配置AsyncImage(modelhttps://example.com/avatar.jpg,contentDescription头像,modifierModifier.size(48.dp).clip(CircleShape),)AsyncImage自动处理请求、缓存、解码、取消。如需自定义配置在根 Composable 中调用setSingletonImageLoaderFactory——这是 CMP 推荐的方式ComposablefunApp(){setSingletonImageLoaderFactory{context-ImageLoader.Builder(context).crossfade(true).build()}// ...}2.4 iOS 内存压力Skia 解码的隐藏陷阱这是一个真实生产环境中的关键问题。CMP 在 iOS 上使用 Skia 解码图片完全绕过了 iOS 的NSCache、ImageIO渐进式解码和系统内存压力回收机制。Skia 把图片解码到SkBitmap中由 Kotlin/Native 内存运行时管理。iOS 对这些分配零可见性发送内存压力通知时无法回收任何东西。在图片密集的列表中滚动 200 张图片常驻内存只增不减最终导致 OOM 崩溃。修复方案的核心是两步第一步用expect/actual桥接 iOS 内存压力通知到 Kotlin 层// commonMainexpectclassMemoryPressureMonitor(){funonLowMemory(callback:()-Unit)}// iosMainactualclassMemoryPressureMonitor{actualfunonLowMemory(callback:()-Unit){NSNotificationCenter.defaultCenter.addObserverForName(UIApplicationDidReceiveMemoryWarningNotification,null,null){callback()}}}第二步实现双层 LRU 缓存。内存压力时优先驱逐已解码的位图保留编码数据——重新解码只需毫秒重新下载需要秒级和带宽funonMemoryPressure(){bitmapLru.evictAll()// 先丢弃昂贵的解码数据// 保留编码数据重新解码比重新下载便宜得多}如果应用有大量图片列表建议在项目早期就实现这套机制而不是等到 OOM 崩溃后再排查。三、主题系统3.1 主题的三个组成部分Material3 的主题由三部分组成颜色方案ColorScheme、排版Typography、形状Shapes。MaterialTheme通过 CompositionLocal 提供这三者。MaterialTheme(colorSchemeAppLightColorScheme,typographyAppTypography,shapesAppShapes,){/* 应用内容 */}3.2 定义颜色方案第一步收集原始颜色令牌用具名常量明确语义valPrimaryColor(0xFF6750A4)valOnPrimaryColor(0xFFFFFFFF)第二步组装 ColorScheme用这些令牌构造浅色和深色方案valAppLightColorSchemelightColorScheme(primaryPrimary,onPrimaryOnPrimary,/* ... */)valAppDarkColorSchemedarkColorScheme(/* 深色版本 */)命名建议用AppLightColorScheme而非LightColorScheme避免与 Material3 类型名混淆。3.3 定义排版与字体在composeResources/font/下放入.ttf文件定义FontFamily再构建TypographyvalAppFontFamilyFontFamily(Font(Res.font.roboto_regular,FontWeight.Normal),Font(Res.font.roboto_bold,FontWeight.Bold),)valAppTypographyTypography(headlineLargeTextStyle(fontFamilyAppFontFamily,fontWeightFontWeight.Bold,fontSize32.sp),bodyLargeTextStyle(fontFamilyAppFontFamily,fontSize16.sp),)命名用AppTypography而非Typography避免类型名冲突。3.4 扩展颜色CompositionLocal 方案Material3 的 ColorScheme 有error但没有success品牌辅助色也无处安放。这些不能放进 ColorScheme而是通过CompositionLocal提供dataclassExtendedColors(valsuccess:Color,valonSuccess:Color,valwarning:Color)valLocalExtendedColorsstaticCompositionLocalOf{ExtendedColors(Color.Unspecified,Color.Unspecified,Color.Unspecified)}通过ColorScheme的扩展属性暴露valColorScheme.success:ColorComposableget()LocalExtendedColors.current.success使用时MaterialTheme.colorScheme.success与内置颜色槽位写法一致。3.5 完整 AppTheme 与深色模式ComposablefunAppTheme(darkTheme:BooleanisSystemInDarkTheme(),content:Composable()-Unit,){valcolorSchemeif(darkTheme)AppDarkColorSchemeelseAppLightColorSchemevalextendedColorsif(darkTheme)AppDarkExtendedColorselseAppLightExtendedColorsCompositionLocalProvider(LocalExtendedColors provides extendedColors){MaterialTheme(colorSchemecolorScheme,typographyAppTypography,contentcontent)}}isSystemInDarkTheme()在所有平台上都能正确读取系统设置。3.6 无障碍对比度容易被忽略的底线在定义颜色方案时必须验证对比度符合 WCAG 标准。确保onPrimary与primary、onSurface与surface等配对的对比度至少为 4.5:1。自定义前景/背景色应为 7:1尤其是小文本。iOS 还提供“增强对比度”无障碍设置通过UIAccessibilityDarkerSystemColorsEnabled检测。如果应用面向有视觉障碍的用户应该提供高对比度调色板在系统启用该设置时切换。四、习题与参考答案本课习题分为三类概念理解1-5 题、代码实践6-11 题、综合设计12-15 题。概念理解习题 1资源目录与限定符支持题目composeResources下有哪些子目录哪个目录不支持限定符参考答案drawable/图像、font/字体、values/字符串、files/其他文件。files/目录不支持限定符且不生成类型安全访问器需要通过Res.readBytes()手动读取。习题 2中文双脚本回退机制题目为什么中文资源必须同时提供values-zh和values-bzhHans只提供后者会怎样参考答案Android 和桌面端系统可能请求不带书写方式的语言区域如zh而非zh-Hans。如果只提供values-bzhHans所有不带脚本的请求都会匹配到它导致资源无法解析而崩溃。必须同时提供values-zh作为无脚本回退。习题 3Coil 的网络引擎选择题目CMP 项目中使用 Coil 时为什么必须用 Ktor 引擎而不是 OkHttp参考答案OkHttp 引擎只在 Android 和 JVM 上可用iOS 和 Web 无法使用。Ktor 本身跨平台coil-network-ktor3在所有目标平台上都能工作。习题 4iOS 图片内存压力的根源题目为什么 CMP 在 iOS 上大量加载图片会导致 OOM 崩溃参考答案CMP 在 iOS 上用 Skia 解码图片完全绕过了 iOS 的NSCache、ImageIO渐进式解码和系统内存压力回收机制。Skia 把图片解码到 Kotlin/Native 堆中iOS 无法感知和回收。图片密集的列表中常驻内存只增不减最终 OOM。习题 5扩展颜色的必要性题目Material3 的 ColorScheme 已经有很多槽位为什么还需要扩展颜色参考答案Material3 覆盖了通用语义角色但应用可能有专属需求。比如“成功”状态——Material3 有error但没有success。这些专属颜色不能放进 ColorScheme需要通过 CompositionLocal 提供。代码实践习题 6添加中文字符串资源题目在values/strings.xml和values-zh/strings.xml中添加应用名称和欢迎消息。参考答案!-- values/strings.xml --resourcesstringnameapp_nameMy App/stringstringnamewelcomeWelcome, %s!/string/resources!-- values-zh/strings.xml --resourcesstringnameapp_name我的应用/stringstringnamewelcome欢迎%s/string/resources习题 7使用字符串模板与复数题目定义一个带占位符的字符串模板和一条复数资源并在 Composable 中使用。参考答案!-- values/strings.xml --resourcesstringnamemessages_countYou have %1$d new messages/stringpluralsnamenew_messageitemquantityone%1$d new message/itemitemquantityother%1$d new messages/item/plurals/resourcesText(stringResource(Res.string.messages_count,5))Text(pluralStringResource(Res.plurals.new_message,1,1))习题 8加载网络图片题目用 Coil 的AsyncImage加载网络图片带圆角裁剪。参考答案AsyncImage(modelhttps://example.com/avatar.jpg,contentDescription头像,modifierModifier.size(64.dp).clip(CircleShape),contentScaleContentScale.Crop,)习题 9定义自定义排版题目用自定义字体定义AppTypographyheadlineLarge用粗体bodyLarge用常规体。参考答案valAppFontFamilyFontFamily(Font(Res.font.roboto_regular,FontWeight.Normal),Font(Res.font.roboto_bold,FontWeight.Bold),)valAppTypographyTypography(headlineLargeTextStyle(fontFamilyAppFontFamily,fontWeightFontWeight.Bold,fontSize32.sp),bodyLargeTextStyle(fontFamilyAppFontFamily,fontSize16.sp),)习题 10定义扩展颜色题目定义ExtendedColors数据类包含success和warning通过 CompositionLocal 提供。参考答案dataclassExtendedColors(valsuccess:Color,valwarning:Color)valLocalExtendedColorsstaticCompositionLocalOf{ExtendedColors(Color.Unspecified,Color.Unspecified)}valAppLightExtendedColorsExtendedColors(successColor(0xFF4CAF50),warningColor(0xFFFF9800),)valColorScheme.success:ColorComposableget()LocalExtendedColors.current.success习题 11iOS 内存压力桥接题目用expect/actual实现一个MemoryPressureMonitoriOS 侧监听内存警告通知。参考答案// commonMainexpectclassMemoryPressureMonitor(){funonLowMemory(callback:()-Unit)}// iosMainactualclassMemoryPressureMonitor{actualfunonLowMemory(callback:()-Unit){NSNotificationCenter.defaultCenter.addObserverForName(UIApplicationDidReceiveMemoryWarningNotification,null,null){callback()}}}综合设计习题 12完整的 AppTheme题目实现AppTheme根据系统深色模式切换同时提供扩展颜色。参考答案ComposablefunAppTheme(darkTheme:BooleanisSystemInDarkTheme(),content:Composable()-Unit,){valcolorSchemeif(darkTheme)AppDarkColorSchemeelseAppLightColorSchemevalextendedColorsif(darkTheme)AppDarkExtendedColorselseAppLightExtendedColorsCompositionLocalProvider(LocalExtendedColors provides extendedColors){MaterialTheme(colorSchemecolorScheme,typographyAppTypography,contentcontent)}}习题 13带深色模式适配的图标题目为 Logo 提供浅色和深色两个版本。参考答案composeResources/ ├── drawable/logo.xml └── drawable-dark/logo.xml统一用Res.drawable.logo系统根据主题自动选择。习题 14带占位图的网络图片题目Coil 加载图片时显示占位图和错误图。参考答案AsyncImage(modelhttps://example.com/image.jpg,contentDescriptionnull,placeholderpainterResource(Res.drawable.placeholder),errorpainterResource(Res.drawable.error),modifierModifier.size(200.dp),)习题 15对比度检查与高对比度主题题目如何验证颜色方案的对比度符合无障碍标准iOS 上如何提供高对比度主题参考答案使用 WCAG 对比度检测工具确保onPrimary/primary对比度至少 4.5:1自定义前景/背景色应为 7:1。iOS 上通过UIAccessibilityDarkerSystemColorsEnabled检测“增强对比度”设置启用时切换到高对比度调色板。五、本课小结资源系统composeResources统一管理多平台资源。drawable放图像font放字体values放字符串files放其他文件不支持限定符。构建时自动生成Res.drawable.xxx、Res.string.xxx类型安全访问器。限定符与本地化优先级为语言 主题 密度。中文必须同时提供values-zh和values-bzhHans否则无脚本请求会崩溃。支持字符串模板和六种量词的复数资源。Coil 图片加载CMP 必须用 Ktor 网络引擎。setSingletonImageLoaderFactory是 CMP 推荐的全局配置方式。iOS 上 Skia 解码绕过系统内存管理图片密集列表需实现expect/actual内存压力桥接和双层 LRU 缓存优先驱逐解码位图、保留编码数据。主题系统由 ColorScheme、Typography、Shapes 组成。扩展颜色通过 CompositionLocal 提供。isSystemInDarkTheme()跨平台读取系统设置。颜色方案必须验证 WCAG 对比度正文 4.5:1自定义前景/背景 7:1。六、下一课预告第9课 测试与调试
阅读完成 · 觉得有帮助?
咨询建站