关于 theming
theming 是一项为 Expo Router 应用添加统一语义颜色系统的技能,提供按平台解析的颜色、明暗模式更新及组件钩子,并指导导航主题集成。
主要功能
- 01统一语义颜色令牌
- 02解析 iOS 和 Android 原生颜色
- 03自动响应明暗模式变化
- 04颜色与品牌钩子
- 05集成导航主题
适用场景
- 01为 Expo Router 应用统一配色
- 02让导航界面匹配应用颜色
- 03支持明暗外观切换
- 04使用原生颜色或固定十六进制值
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ code-with-beto/ skills --skill theming安装后在 Claude Code 中输入/theming确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/colors.tsconfig.tsThemeContext.tsx详细介绍
Expo 统一主题
只需设置一套颜色系统,即可同时适用于 iOS 和 Android。用户无需在整个应用中到处使用 Color.ios.*、Color.android.dynamic.*(或硬编码的十六进制值),而是可以使用语义化标记(background、text、link 等)。这些标记会根据平台解析为对应的原生颜色,自动随深色/浅色模式切换,并通过单个 useColors() hook 读取。
这是 Platano 模板采用的模式,现已打包,可直接用于任何 Expo Router 应用。
安装内容
三个文件(包含在 assets/ 中,请根据项目调整):
config.ts— 唯一事实来源。语义系统标记设为"native"或十六进制值,另含品牌颜色。这是用户日常唯一需要编辑的文件。colors.ts— 根据平台将标记解析为实际颜色,并提供getSystemColor、useSystemColors、品牌辅助函数和getNavigationTheme。ThemeContext.tsx—ColorsProvider,以及基于 Reactuse构建的useColors()/useTheme()/useBrand()hooks。
此外,还需稍微修改根目录的 _layout.tsx,以连接各个 provider。
让这一模式生效的两个关键点
请向用户说明以下内容,因为这是整个模式的核心,也是最容易出错的两个地方:
-
主题变更时重新渲染。 原生颜色和 Material You 颜色是平台颜色对象,而不是 React 可以进行差异比较的字符串。用于指示主题已变更的信号是
useColorScheme()。因此,即使解析器并不读取颜色方案,也要将其作为参数传入:这样就将深色/浅色模式设为显式依赖项,用户切换模式时,值就会重新计算。ColorsProvider会在靠近根节点的位置读取一次useColorScheme(),因此切换模式会触发所有使用方重新渲染。 -
两个 ThemeProvider,各司其职。
ColorsProvider(此技能提供)通过useColors()为组件提供颜色。导航的ThemeProvider则用于设置导航界面(标题栏、标签栏、默认背景)的主题。两者都位于根节点。请使用getNavigationTheme(dark)连接导航的 ThemeProvider,使导航界面与标记保持一致。不要混淆两者,也不要遗漏其中任何一个。对于 Expo SDK 56+,导航主题相关内容(ThemeProvider、DarkTheme、DefaultTheme、Theme)从expo-router/react-navigation导入;对于 较旧的 SDK,则从@react-navigation/native导入(请参阅步骤 1)。
步骤 1:确认项目、SDK 和原生 Color API
此技能适用于 Expo SDK 56 或更新版本上的 Expo Router 应用。SDK 56 开始同时提供 Color API(iOS 系统颜色和 Android Material You)以及模板所用的 expo-router/react-navigation 重新导出。在使用 "native" 标记前,请先检查 SDK 和 Color API。
# Find the project's app dir and root layout (root-level or under src/)
ls app/_layout.tsx src/app/_layout.tsx 2>/dev/null
# Check the installed Expo SDK major version
node -e "console.log('expo', require('expo/package.json').version)" 2>/dev/null
# Confirm expo-router exports the Color API
node -e "console.log('Color export:', !!require('expo-router').Color)" 2>/dev/null
grep -rl "android.dynamic\|ios.systemBackground" node_modules/expo-router/build 2>/dev/null | head -1
根据检查结果进行分支处理:
- SDK 56+(目标版本): 按原样使用模板。导航主题相关内容从
expo-router/react-navigation导入。 - 低于 SDK 56: 需要做两项调整。
- 在
colors.ts和_layout.tsx中,将ThemeProvider、DarkTheme、DefaultTheme和type Theme改为从@react-navigation/native导入,而不是从expo-router/react-navigation导入。(较早版本的 Expo Router 不会重新导出这些内容,因此需要直接从 React Navigation 导入。) ColorAPI 可能不存在。可以建议升级到 SDK 56,也可以使用十六进制回退方案:保留这些文件,但将config.ts中的system标记设为十六进制值,而不是"native"(无法使用 Material You,但其他功能均可正常运行)。getBrandColors仍可使用;没有Color时,isAndroidDynamic会解析为 false。
- 在
还要检查项目的 tsconfig.json,确认导入别名(通常是 @/* → src/* 或项目根目录)。编写导入语句时应匹配该别名。如果没有别名,则使用相对路径。
第 2 步:确定文件的存放位置
将这三个文件放在同一个文件夹中,这样它们的相对导入路径
(./config、./colors)就能正常使用。按照项目的目录结构放置:
- 应用位于
src/app/下 → 创建src/theme/。 - 应用位于根目录的
app/下 → 在根目录创建theme/。
先检查是否已有 theme/colors 模块或 ThemeProvider。如果应用已经有主题设置,不要覆盖。阅读现有内容,向用户说明重叠之处,然后选择合并到现有设置中,或将这些文件放到名称清晰的文件夹下,让用户自行迁移。绝不要盲目覆盖。
第 3 步:创建文件
将随附的三个模板复制到选定文件夹,并调整:
- 导入别名/相对路径,使其符合项目设置;
config.ts中的system令牌和brand颜色,使其符合用户的品牌(询问用户品牌的主色/强调色,或者保留合理的默认值,并告知用户在哪里修改)。
读取每个资源文件,并将其写入项目(不要只创建符号链接——这些文件现在属于用户):
assets/config.ts→<theme>/config.tsassets/colors.ts→<theme>/colors.tsassets/ThemeContext.tsx→<theme>/ThemeContext.tsx
保留注释。它们说明了重新渲染技巧以及品牌/动态行为,这正是学习者需要在自己的代码库中了解的内容。
第 4 步:连接根布局
谨慎修改现有的 _layout.tsx——保留已有的每个 provider、screen 和 option。目标结构是在 ColorsProvider 中包裹整个组件树,并在可读取颜色方案的内部组件中设置导航主题:
// SDK 56+: from "expo-router/react-navigation". Older SDKs: "@react-navigation/native".
import { ThemeProvider } from "expo-router/react-navigation";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useColorScheme } from "react-native";
import { getBrandColors, getNavigationTheme } from "@/theme/colors";
import { ColorsProvider } from "@/theme/ThemeContext";
export default function RootLayout() {
return (
<ColorsProvider>
{/* keep any existing providers (QueryClient, RevenueCat, gestures...) here */}
<RootLayoutInner />
</ColorsProvider>
);
}
function RootLayoutInner() {
const dark = useColorScheme() === "dark";
const { primary } = getBrandColors(dark);
return (
<>
<ThemeProvider value={getNavigationTheme(dark, primary)}>
{/* keep the project's existing <Stack> / screens exactly as they were */}
<Stack />
</ThemeProvider>
<StatusBar style={dark ? "light" : "dark"} />
</>
);
}
集成规则:
ColorsProvider必须位于所有调用useColors()的内容上层。将它放在RootLayout的顶部或附近最稳妥。- 如果文件已经拆分为
RootLayout和一个内部组件,就在该内部组件中添加ThemeProvider和useColorScheme(),不要新建内部组件。 - 如果未安装
expo-status-bar,且项目也没有以其他方式管理状态栏,可以省略StatusBar这一行。
第 5 步:验证
- 如果可以,进行类型检查:
npx tsc --noEmit(预期这三个文件不会产生新错误;最常见的问题是导入别名错误)。 - 告诉用户运行应用,然后切换操作系统外观(或模拟器的深色/浅色模式),确认颜色和导航栏样式会实时更新。在实体 Android 设备上,更换壁纸应该会改变 Material You 调色板。
第 6 步:展示用法
提供一段简短的用法示例(并表示如果用户想立即查看效果,可以将一个主题示例放入某个屏幕):
import { View, Text } from "react-native";
import { useColors, useBrand } from "@/theme/ThemeContext";
export function Example() {
const { background, text, secondaryText, separator } = useColors();
const { primary } = useBrand();
return (
<View style={{ flex: 1, backgroundColor: background, padding: 16 }}>
<Text style={{ color: text, fontSize: 20, fontWeight: "600" }}>
Unified theming
</Text>
<Text style={{ color: secondaryText }}>
Same tokens on iOS and Android.
</Text>
<View style={{ height: 1, backgroundColor: separator, marginVertical: 12 }} />
<Text style={{ color: primary }}>Brand accent</Text>
</View>
);
}
在 React 之外(例如 toast 或一次性样式中),使用非 hook 访问器:
import { getSystemColor } from "@/theme/colors";
const card = getSystemColor("secondaryBackground") as string;
自定义
- **在所有地方更改某个颜色:**编辑
config.ts。将令牌设为十六进制值即可固定颜色,设回"native"则跟随操作系统。 - 关闭 Material You,在 Android 上使用品牌颜色:设置
brand.useAndroidDynamic: false,这样所有平台都会应用十六进制品牌色。 - **添加令牌:**在
config.ts的system中添加令牌,然后在colors.ts的getNativeDefault中添加对应的 iOS/Android/默认值,并在resolveSystemColors中添加一行。TypeScript 会指出遗漏的内容。
说明
- 此设置只安装颜色和品牌配置。它有意不添加间距或圆角预设,也不添加持久化功能;除非用户提出要求,否则请保持精简。
- 这些文件现在属于用户的代码。鼓励他们阅读注释并进行调整,而不是将其视为黑盒。这正是本技能配套课程的教学目标。