4.9 KiB
4.9 KiB
【Demo】品牌换肤(两级主题模型)
源文件:
lib/pages/demo/theme_demo.dart| 分类:基础布局 一句话:换品牌 = 一级换整套色板 + 二级换页面细节,一套代码全 App 变色。
一、这个 Demo 解决什么问题
教育 App 常有"多品牌运营"需求:不同机构定制不同主色、页面配色、甚至标题文案。 本 Demo 演示项目核心的两级主题模型:
一级 = 品牌:seedColor 生成整套 Material 色板(全 App 共享)
二级 = 页面细节(PageTheme):按页面层级(home/detail/result)
区分背景色 / 头部色 / 强调色 / 专属 banner / 标题文案
页面只声明"我是哪个层级",细节从品牌对象按层级查出来: 换品牌 → 一级色板全换 + 二级细节全换(一套代码)。
二、怎么运行 / 演示内容
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 的原理
ThemeData(colorSchemeSeed: _brand.seedColor)
传一个种子色,Material 自动生成整套 ColorScheme(primary/secondary/tertiary...), 没写死颜色的 Material 组件全部自动跟随(FilledButton/Checkbox/Switch/进度条/聚焦输入框)。 验证页见 seed_palette_demo.md。
⚠ colorSchemeSeed 生成的 primary 不是精确色值(Material 会调和), 要求精确用
ColorScheme.fromSeed(...)后手动覆盖 primary。
3. 二级:PageTheme 按层级查询
// 页面只声明层级(本 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 的切换法)
return Theme(
data: ThemeData(colorSchemeSeed: _brand.seedColor),
child: Scaffold( ... ), // 页面内组件都从局部主题取色
);
真实项目把
_brand换成 Riverpod Provider + MaterialApp 的 theme,切换点移到根层。
四、关键代码剖析
// 品牌切换(注册表驱动,新增品牌自动出现)
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('本页主按钮'),
),
]),
)
五、踩坑记录
- colorSchemeSeed 生成的 primary 不精确 —— 需要精确色值时手动覆盖;
- 局部
Theme(data:)必须包在 Scaffold 外面,AppBar 才能取到(包在 body 里 AppBar 取不到); - PNG 位图不跟主题着色 —— 能用矢量(IconData)就别放多份图;
- 深色模式要单独配
darkTheme+themeMode,不是简单换个 seed; .w/.h/.sp的组件不能 const(const_eval_extension_method)。
六、扩展思考
- 持久化:品牌选择存 shared_preferences,启动时读出来传给 MaterialApp 的 theme;
- 动态切换动画:
AnimatedTheme包 MaterialApp,换主题有过渡动画; - 与 seed_palette_demo.md 对照:一个验证"seed 生效",一个演示"两级模型的消费";
- 更重的多主题需求可引入
flex_color_scheme包,本项目的轻量模型够用且无依赖。