需求
个别厂商需求话定制,要求提供不同包名、logo、图片资源、代码逻辑的apk
实现方式
1、拉新分支管理
从主线分支拉不同OEM版本的分支,在新分支上改动
分支图示:
冲突
-
单次提供需求可以满足
-
频繁OEM需求场景下,主线代码已更改:
1、从主线分支拉取,从新修改,工作量重复
2、维护OEM分支代码,跟随主线分支代码合并改动,工作量大、重复
2、Flavor渠道方式管理
原生gradle配置文件下新建Flavor渠道,按渠道打包,原生代码目录下新建对应渠道名资源文件夹,替换资源。Flutter通过Channel方式读取原生Target,代码中判断Target执行代码逻辑
示例代码:
if (GetIt.I<AppConfigUseCase>().isSmartHome){
/// do something
} else if (GetIt.I<AppConfigUseCase>().isBotslab){
/// do otherthing
}
冲突
-
不新增更多Target情况下,可以满足
-
多Target场景下,代码逻辑复杂,岔口太多
优化
Flutter中读yaml配置文件方式
yaml配置文件示例:
name: botslab
description: target config file.
appTarget: botslab
##设备背景占位图
device_image_placeholder: botslab
#首页背景图
home_header_bg_screen:
bg: botslab
#首页入口
main_entry_model:
#是否含有商城入口
hasMall: false
#首页相关
home_screen:
getHeaderInfo: false
#背景样式
# type1: lottie
# type2: image
home_screen_template: image
#用户中心
user_profile_screen:
#使用问题
user_profile_item_problem: false
headBg: botslab
DeviceUseCase:
hasVirtualDevices: false
#设置页开关
settings_screen:
#区域与语言
user_setting_item_group_region_language: true
#网络检测
user_setting_item_net_detect: true
#地区
user_setting_item_region: true
代码逻辑岔路合并
if (GetIt.I<AppConfigUseCase>().targetConfigBean["main_entry_model"]["hasMall"] == true)
根据运行时传入的target解析对应的yaml配置文件
var configFileName = 'packages/service_common/assets/target_config_';
@override
Future<void> initTargetConfig() async {
final target = _appConfigData["target"] as String;
final targetFileName = configFileName + target + ".yaml";
final data = await rootBundle.loadString(targetFileName);
_targetConfigBean = loadYaml(data) as Map;
}
按功能需求配置yaml配置文件,放在指定目录
难点
1、yaml配置文件仅方便配置开关类功能需求,难以描述更加定制化需求,仍无法和target彻底解耦
比如代码中不同逻辑需求是这样的:
if (GetIt.I<AppConfigUseCase>().targetConfigBean["home_header_bg_screen"]["bg"] == "smarthome") {
return Transform(
alignment: Alignment.topCenter,
transform: Matrix4.translationValues(0, offsetY, 0)..scale(scaleY),
child: LottieBuilder.asset(
"assets/lottie/background.json",
package: "feature_main",
width: double.infinity,
height: botslabHeaderHeight,
fit: BoxFit.cover,
),
);
} else {
return Transform(
alignment: Alignment.topCenter,
transform: Matrix4.translationValues(0, offsetY, 0)..scale(scaleY),
child: Image(
image: ImageAssets.botslabHomeBg,
height: botslabHeaderHeight,
width: double.infinity,
fit: BoxFit.cover,
),
);
}
配置文件是这样的
##设备背景占位图
device_image_placeholder: botslab
#首页背景图
home_header_bg_screen:
bg: botslab
逐渐有点坏代码的味道
配置文件梳理
1、梳理分类变体配置类型
按需求功能点类型区分,配置文件中配置类型目前可分为三种类型
a、功能开关是否打开
要求不同target设置项列表展示差异,部分功能入口显示隐藏控制
if(isOpen)
BuildItem(),
b、图片资源显示差异
图片资源路径不同,例如placeHolder,botslab 加载"assets/images/smarthome_img_place_holder.png"
目录下图片,smarthome加载"assets/images/botslab_img_place_holder.png"
目录下图片
if(target1){
AssetImage("assets/images/smarthome_img_place_holder.png",package:"res_common");
} else if(target2){
AssetImage("assets/images/botslab_img_place_holder.png",package:"res_common");
}
c、代码逻辑执行差异
对于某target要单独调用一只接口或执行某处理逻辑
if(target1){
getHeaderInfo();
}
d、控件展示差异
不同target返回不同widget组件
if(target1){
return Widget1()
} else if(target2){
return Widget2()
}
结论
配置文件方便做功能开关、密码、域名、资源路径类静态替换设置控制a、b、c,不适合做代码逻辑类控制如d这种代码和target版本强相关类型代码,无法彻底解耦。
试想一下,如果将类似代码逻辑过多塞进配置文件,那它和真正的代码文件又有什么分别。
yaml:我只是一个小小的配置文件,为什么对我的要求要这么多?why
所以,采用所有类似入口均设置一项配置name: targetName
代码中沿用之前判断方式
参考springboot项目中配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
redis:
database: 0
host: localhost
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 0
2、确定配置对象结构和访问方式
按配置文件声明的控制项设计配置对象结构,按键值对映射,全部按一级展开成实例对象,加注释和回车方式归并。后续有分类需求可按二级开关string: Map<string,dynamic>
控制。
配置对象
class TargetConfigBean{
String appTarget;
bool hasMainEntryMall;
bool hasHomeScreenGetHeaderInfo;
bool hasUserProfileItemProblem;
bool hasVirtualDevices;
bool hasSettingsItemGroupRegionLanguage;
bool hasSettingsItemNetDetect;
bool hasSettingsItemRegion;
factory TargetConfigBean.fromJson(Map<dynamic,dynamic> json) =>
_targetConfigBeanFromJson(json);
}
yaml文件解析
TargetConfigBean _targetConfigBeanFromJson(Map<dynamic, dynamic> json) {
final bean = TargetConfigBean();
bean.appTarget = json["appTarget"] as String;
bean.hasMainEntryMall = json["main_entry_model"]["hasMall"] as bool;
bean.hasHomeScreenGetHeaderInfo = json["home_screen"]["getHeaderInfo"] as bool;
bean.hasUserProfileItemProblem = json["user_profile_screen"]["user_profile_item_problem"] as bool;
bean.hasVirtualDevices = json["DeviceUseCase"]["hasVirtualDevices"] as bool;
bean.hasSettingsItemGroupRegionLanguage = json["settings_screen"]["user_setting_item_group_region_language"] as bool;
bean.hasSettingsItemNetDetect = json["settings_screen"]["user_setting_item_net_detect"] as bool;
bean.hasSettingsItemRegion = json["settings_screen"]["user_setting_item_region"] as bool;
return bean;
}
访问方式
/// a、b、c类接入
if (GetIt.I<AppConfigUseCase>().targetConfigBean.hasMainEntryMall){
/// open
}
/// d类接入
if (GetIt.I<AppConfigUseCase>().targetConfigBean.appTarget == AppTarget.smarthome.targetName) {
/// do something
} else if (GetIt.I<AppConfigUseCase>().targetConfigBean.appTarget == AppTarget.botslab.targetName) {
/// do otherthing
}
调整后yaml配置
name: botslab
description: target config file.
appTarget: botslab
#首页入口
main_entry_model:
#是否含有商城入口
hasMall: false
#首页相关
home_screen:
getHeaderInfo: false
#用户中心
user_profile_screen:
#使用问题
user_profile_item_problem: false
DeviceUseCase:
hasVirtualDevices: false
#设置页开关
settings_screen:
#区域与语言
user_setting_item_group_region_language: true
#网络检测
user_setting_item_net_detect: true
#地区
user_setting_item_region: true
展望
探索编译期读配置文件方案