Files
2026-09-04 15:22:20 +08:00

129 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】品牌换肤(两级主题模型)
> 源文件:`lib/pages/demo/theme_demo.dart` | 分类:基础布局
> 一句话:换品牌 = 一级换整套色板 + 二级换页面细节,一套代码全 App 变色。
---
## 一、这个 Demo 解决什么问题
教育 App 常有"多品牌运营"需求:不同机构定制不同主色、页面配色、甚至标题文案。
本 Demo 演示项目核心的**两级主题模型**:
```
一级 = 品牌:seedColor 生成整套 Material 色板(全 App 共享)
二级 = 页面细节(PageTheme):按页面层级(home/detail/result)
区分背景色 / 头部色 / 强调色 / 专属 banner / 标题文案
```
页面只声明"我是哪个层级",细节从品牌对象按层级查出来:
**换品牌 → 一级色板全换 + 二级细节全换(一套代码)**。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: ThemeDemoPage()));
```
- 顶部 ChoiceChip 切换品牌(从注册表 `brandThemes` 自动生成按钮,新增品牌自动出现);
- "一级"区:显示 seed 色与生成的 primary 对比 + 跟随主题的按钮/进度条;
- "二级"区:home/detail/result 三个层级各渲染一个页面骨架预览(背景/头部/强调色/文案全跟随品牌)。
---
## 三、核心知识点(新手向)
### 1. 主题的两层结构(模型已解耦,见 `core/theme/`)
| 层 | 内容 | 存放 |
|---|---|---|
| 模型 + 注册表 | `BrandTheme` / `PageTheme` / `PageLevel` + `brandThemes` | `core/theme/brand_theme.dart` |
| 每品牌配置 | 只有具体色值/文案 | `core/theme/brands/xxx_theme.dart` |
新增一个品牌 = 新建一个配置文件 + 注册表加一行,**业务代码零改动**。
### 2. 一级:colorSchemeSeed 的原理
```dart
ThemeData(colorSchemeSeed: _brand.seedColor)
```
传一个种子色,Material 自动生成整套 ColorScheme(primary/secondary/tertiary...),
**没写死颜色的 Material 组件全部自动跟随**(FilledButton/Checkbox/Switch/进度条/聚焦输入框)。
验证页见 [seed_palette_demo.md](seed_palette_demo.md)。
> ⚠ colorSchemeSeed 生成的 primary 不是精确色值(Material 会调和),
> 要求精确用 `ColorScheme.fromSeed(...)` 后手动覆盖 primary。
### 3. 二级:PageTheme 按层级查询
```dart
// 页面只声明层级(本 demo 直接取,真实项目从路由/参数拿)
final page = _brand.pages[PageLevel.home]!;
// 使用:背景 page.bg、头部 page.headerBg、强调 page.accent、banner page.banner
```
- banner 存资源路径(如 `assets/img/banner_sky.png`),页面 `Image.asset(page.banner!)` 即可跟随换图;
- 本 demo 没准备两套图片,用"强调色渐变块"模拟 banner 占位(代码里标注了替换点)。
### 4. 局部主题覆盖(不依赖全局 MaterialApp 的切换法)
```dart
return Theme(
data: ThemeData(colorSchemeSeed: _brand.seedColor),
child: Scaffold( ... ), // 页面内组件都从局部主题取色
);
```
> 真实项目把 `_brand` 换成 Riverpod Provider + MaterialApp 的 theme,切换点移到根层。
---
## 四、关键代码剖析
```dart
// 品牌切换(注册表驱动,新增品牌自动出现)
Wrap(
spacing: 8,
children: [
for (final b in brandThemes)
ChoiceChip(
label: Text(b.name),
selected: _brand.name == b.name,
onSelected: (_) => setState(() => _brand = b),
),
],
)
// 二级细节消费示例(页面骨架预览)
Container(
color: page.bg, // ← 二级:背景
child: Column(children: [
Container(
color: page.headerBg, // ← 二级:头部色
child: Text(page.title), // ← 二级:标题文案
),
// banner 占位:真实项目替换成 Image.asset(page.banner!)
Container(decoration: BoxDecoration(gradient: LinearGradient(colors: [page.accent, page.headerBg]))),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: page.accent), // ← 二级:强调色
child: const Text('本页主按钮'),
),
]),
)
```
---
## 五、踩坑记录
1. **colorSchemeSeed 生成的 primary 不精确** —— 需要精确色值时手动覆盖;
2. **局部 `Theme(data:)` 必须包在 Scaffold 外面**,AppBar 才能取到(包在 body 里 AppBar 取不到);
3. **PNG 位图不跟主题着色** —— 能用矢量(IconData)就别放多份图;
4. 深色模式要单独配 `darkTheme` + `themeMode`,不是简单换个 seed;
5. `.w/.h/.sp` 的组件不能 const(`const_eval_extension_method`)。
---
## 六、扩展思考
- 持久化:品牌选择存 shared_preferences,启动时读出来传给 MaterialApp 的 theme;
- 动态切换动画:`AnimatedTheme` 包 MaterialApp,换主题有过渡动画;
- 与 [seed_palette_demo.md](seed_palette_demo.md) 对照:一个验证"seed 生效",一个演示"两级模型的消费";
- 更重的多主题需求可引入 `flex_color_scheme` 包,本项目的轻量模型够用且无依赖。