本文介紹了一次換膚需求經(jīng)歷屎飘。
背景
產品想要在一些大型活動時在社區(qū)里換膚够滑,換換背景圖蛤高、背景色這些蚣旱,其實和之前適配多業(yè)務視覺風格有相似之處,當時也把通用資源整理歸類戴陡,直接同名同類型替換即可塞绿。
在這個場景下雖然可以無需開發(fā)通過熱更來替換資源,但之后每次活動上線都要開發(fā)配合發(fā)熱更包恤批,可能還要支持除圖以外的其他配置項异吻,在后期也會有隱形開發(fā)成本。所以最好前端提前預埋配置項,之后運營直接在管理端配置诀浪,可一勞永逸棋返。
方案
和產品梳理發(fā)現(xiàn)配置項高達40+個,時間原因先做一部分雷猪,如此在前期就要考慮好如何設計方便后續(xù)快速開發(fā)上線睛竣。
首先需要新增一個皮膚配置管理器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)
躏惋,避免并發(fā)多次請求考慮和其他請求做合并。
然后需要適配圖片組件嚷辅,這里盡可能保持組件的通用性以及降低外部使用成本簿姨,兼容了各種默認圖樣式,外部只要多傳入一個圖片配置項和是否需要開啟換膚開關即可簸搞。換膚圖片組件SkinImageView
具體如下:
class SkinImageView extends StatelessWidget {
final ValueNotifier<String> imgNotifier; // from SkinConfigMgr
final String defImgUrl; // 原圖片路徑
final double size; // 圖片尺寸扁位,這里填 width,若 height 和 width 不一致還需要給下 height
final bool fromNetwork; // 是否網(wǎng)絡圖
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);
}
// 網(wǎng)絡圖
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;
},
);
}
}
接著繼續(xù)適配顏色組件趁俊,由于原來大部分是通過Container
組件實現(xiàn)顏色的域仇,這里還是參考這種方式,相當于在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 不能同時設置,優(yōu)先用 decoration
decoration: decoration,
foregroundDecoration: foregroundDecoration,
width: width,
height: height,
constraints: constraints,
margin: margin,
transform: transform,
transformAlignment: transformAlignment,
child: child,
clipBehavior: clipBehavior,
);
},
);
}
}
可以看到實際替換代碼就非常簡單了:
總結
對于不同的換膚需求提供不同的方案怔软,在后面擴展時也能提供更多的選擇垦细。