Files
App.K12.Student.Miniapp_Flu…/docs/demos/theme_demo.md
T
2026-09-04 15:22:20 +08:00

4.9 KiB
Raw Blame History

【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('本页主按钮'),
    ),
  ]),
)

五、踩坑记录

  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 生效",一个演示"两级模型的消费";
  • 更重的多主题需求可引入 flex_color_scheme 包,本项目的轻量模型够用且无依赖。