Flutter|页面换肤实践

本文介绍了一次换肤需求经历。

背景

产品想要在一些大型活动时在社区里换肤,换换背景图、背景色这些,其实和之前适配多业务视觉风格有相似之处,当时也把通用资源整理归类,直接同名同类型替换即可。

在这个场景下虽然可以无需开发通过热更来替换资源,但之后每次活动上线都要开发配合发热更包,可能还要支持除图以外的其他配置项,在后期也会有隐形开发成本。所以最好前端提前预埋配置项,之后运营直接在管理端配置,可一劳永逸。

方案

和产品梳理发现配置项高达40+个,时间原因先做一部分,如此在前期就要考虑好如何设计方便后续快速开发上线。

首先需要新增一个皮肤配置管理器SkinConfigMgr用来拉取和保存配置内容,由于配置拉取是异步的,为了能及时刷新界面这里配置都是用ValueNotifier持有,例如有这样几个配置项:

class SkinConfigMgr {
  static SkinConfigMgr? _instance;
  SkinConfigMgr._();
  static SkinConfigMgr get instance => _instance ??= SkinConfigMgr._();

  ValueNotifier<String> mainNavBgColor = ValueNotifier(""); // 首页顶部导航栏背景色
  ValueNotifier<String> mainBgImg = ValueNotifier(""); // 首页背景图
  ValueNotifier<String> mainBgColor = ValueNotifier(""); // 首页背景色

  // 初始化
  void initSkinConfigs(ClientThemeSetting settings) {
    mainNavBgColor.value = settings.mainNavBgColor;
    mainBgImg.value = settings.mainBgImg;
    mainBgColor.value = settings.mainBgColor;
  }
}

因为是全局配置,要在app启动时就拉取请求,然后调用初始化即可SkinConfigMgr.instance.initSkinConfigs(apiReply.themeSetting),避免并发多次请求考虑和其他请求做合并。

然后需要适配图片组件,这里尽可能保持组件的通用性以及降低外部使用成本,兼容了各种默认图样式,外部只要多传入一个图片配置项和是否需要开启换肤开关即可。换肤图片组件SkinImageView 具体如下:

class SkinImageView extends StatelessWidget {
  final ValueNotifier<String> imgNotifier; // from SkinConfigMgr
  final String defImgUrl; // 原图片路径
  final double size; // 图片尺寸,这里填 width,若 height 和 width 不一致还需要给下 height
  final bool fromNetwork; // 是否网络图
  final double? height;
  final String? package;
  final BoxFit? fit;
  final IconData? icon;
  final bool supportSkinConfig;

  SkinImageView(
    this.imgNotifier,
    this.defImgUrl,
    this.size,
    this.fromNetwork, {
    this.height,
    this.package,
    this.fit,
    this.icon,
    this.supportSkinConfig = true,
  });

  Widget _getDefView() {
    if (defImgUrl.isEmpty) {
      return SizedBox.shrink();
    }
    // icon图标
    if (icon != null) {
      return Icon(icon, size: size);
    }
    // 网络图
    if (fromNetwork) {
      return ImageView(
        url: defImgUrl,
        width: size,
        height: height ?? size,
        fit: fit,
      );
    }
    // 本地svg
    if (defImgUrl.toLowerCase().endsWith("svg")) {
      return SvgPicture.asset(
        defImgUrl,
        width: size,
        height: height ?? size,
        package: package,
        fit: fit ?? BoxFit.contain,
      );
    }
    // 本地其他图
    return Image.asset(
      defImgUrl,
      width: size,
      height: height ?? size,
      package: package,
      fit: fit,
    );
  }

  @override
  Widget build(BuildContext context) {
    Widget defView = _getDefView();
    if (!supportSkinConfig) {
      return defView;
    }
    return ValueListenableBuilder<String>(
      valueListenable: imgNotifier,
      builder: (BuildContext context, String value, Widget? widget) {
        return value.isNotEmpty
            ? ImageView(
                url: value,
                width: size,
                height: height ?? size,
                fit: fit,
              )
            : defView;
      },
    );
  }
}

接着继续适配颜色组件,由于原来大部分是通过Container组件实现颜色的,这里还是参考这种方式,相当于在Container基础上又包装了一层,外部只要多传入配置项和换肤开关即可。换肤颜色组件SkinColorContainer 具体如下:

class SkinColorContainer extends StatelessWidget {
  final ValueNotifier<String> colorNotifier; // from SkinConfigMgr
  final Color? color; // 原颜色要放在 color 属性而不是 decoration
  final AlignmentGeometry? alignment;
  final EdgeInsetsGeometry? padding;
  final Decoration? decoration;
  final Decoration? foregroundDecoration;
  final double? width;
  final double? height;
  final BoxConstraints? constraints;
  final EdgeInsetsGeometry? margin;
  final Matrix4? transform;
  final AlignmentGeometry? transformAlignment;
  final Widget? child;
  final Clip clipBehavior;
  final bool supportSkinConfig;

  SkinColorContainer(
    this.colorNotifier,
    this.color, {
    this.alignment,
    this.padding,
    this.decoration,
    this.foregroundDecoration,
    this.width,
    this.height,
    this.constraints,
    this.margin,
    this.transform,
    this.transformAlignment,
    this.child,
    this.clipBehavior = Clip.none,
    this.supportSkinConfig = true,
  });

  static Color? getColor(String colorStr, Color? defColor,
      {bool supportSkinConfig = true}) {
    if (!supportSkinConfig ||colorStr.isEmpty) {
      return defColor;
    }
    try {
      colorStr = colorStr.toLowerCase().replaceAll("#", "");
      if (colorStr.length == 6) {
        colorStr = "ff" + colorStr;
      }
      return Color(int.parse(colorStr, radix: 16));
    } catch (e) {
      return defColor;
    }
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<String>(
      valueListenable: colorNotifier,
      builder: (BuildContext context, String value, Widget? widget) {
        return Container(
          alignment: alignment,
          padding: padding,
          color: decoration != null || foregroundDecoration != null
              ? null
              : getColor(value, color, supportSkinConfig: supportSkinConfig),
          // decoration 和 color 不能同时设置,优先用 decoration
          decoration: decoration,
          foregroundDecoration: foregroundDecoration,
          width: width,
          height: height,
          constraints: constraints,
          margin: margin,
          transform: transform,
          transformAlignment: transformAlignment,
          child: child,
          clipBehavior: clipBehavior,
        );
      },
    );
  }
}

可以看到实际替换代码就非常简单了:

总结

对于不同的换肤需求提供不同的方案,在后面扩展时也能提供更多的选择。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容