# 【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` 包,本项目的轻量模型够用且无依赖。