多应用配置
12147什么是多应用配置
多应用配置是指在同一个Flutter项目中,可以配置多个Android或iOS应用程序的不同设置和属性,以便为每个应用程序创建不同的应用程序ID、应用程序名称、应用程序图标等。
它允许开发人员在同一个Flutter项目中创建多个应用程序,而不必为每个应用程序单独创建一个新项目。这样做可以减少代码重复和维护成本,并且可以更快地开发和部署多个应用程序。
此外,使用Flutter的多应用配置还可以为不同的应用程序创建不同的主题、语言、字体等自定义设置。这意味着开发人员可以更轻松地为不同的用户群体创建不同的应用程序。
Flutter的多应用配置
多应用配置
在 /jianghuAppBrowser/jianghuBrowser/lib/main.dart
中配置。
主要包含以下配置:
"backgroundNotice": { //手机通知栏是否开启
"org.fsll.duoxing5": false,
"org.fsll.jianghu3": false,
},
"openSocket": { //是否打开Socket (在我们的项目中,使用的webSocket)
"org.fsll.duoxing5": false,
"org.fsll.jianghu3": false,
},
"openBottomBar": { //地址栏是否显示,默认都显示
"org.fsll.duoxing5": true,
"org.fsll.jianghu3": true,
},
"defaultIcons": { //icon
"org.fsll.duoxing5": "assets/logos/dx5_launcher.png",
"org.fsll.jianghu3": "assets/logos/jianghu_launcher.png",
},
"defaultHost": { //webview url
"org.fsll.duoxing5": "https://md.jhdx.org/",
"org.fsll.jianghu3": "https://www.jianghu03.com/",
},
"primaryColor": { //主题色
"org.fsll.duoxing5": const Color(0xFF00579c),
"org.fsll.jianghu3": const Color(0xFFff5300),
},
"resolverJsons": { //加密后的域名,需通过接口获取到域名信息,然后解密,会覆盖defaultHost配置
"org.fsll.duoxing5": [
"https://md20211227-1301849697.cos.accelerate.myqcloud.com/duoxing_v5.json",
"https://md-20220505.oss-accelerate.aliyuncs.com/duoxing_v5.json"
],
"org.fsll.jianghu3": [
'https://md20211227-1301849697.cos.accelerate.myqcloud.com/jianghu_v3.json',
'https://md-20220505.oss-accelerate.aliyuncs.com/jianghu_v3.json'
],
},
"appIds": { //appId
"org.fsll.duoxing5": "duoxing",
"org.fsll.jianghu3": "jianghu3",
},
"appName": { //app名称
"org.fsll.duoxing5": "多星5",
"org.fsll.jianghu3": "江湖3",
},
resolverJsons 获取与解密的逻辑参考 /jianghuAppBrowser/jianghuBrowser/lib/Utils/ResolverListUtil.dart
主要逻辑如下:
static Future<void> checkAndApplyConfig(context, int index, {onError, onDone}) async {
if (Constants.isIosDev) {
RenderKey.hostUrl.value = Constants.config.defaultHost['${Constants.packageInfo.packageName}.dev'];
Constants.defaultHostUrl = RenderKey.hostUrl.value;
RenderKey.urlEditController.text = RenderKey.hostUrl.value!;
} else if (Constants.packageInfo.packageName.endsWith('.dev')) {
RenderKey.hostUrl.value = Constants.config.defaultHost[Constants.packageInfo.packageName];
Constants.defaultHostUrl = RenderKey.hostUrl.value;
RenderKey.urlEditController.text = RenderKey.hostUrl.value!;
} else if(Constants.resolverList.isEmpty) {
RenderKey.hostUrl.value = Constants.config.defaultHost[Constants.packageInfo.packageName];
Constants.defaultHostUrl = RenderKey.hostUrl.value;
RenderKey.urlEditController.text = RenderKey.hostUrl.value!;
} else {
await ResolverListUtil.checkResolverConfig(context);
}
if (RenderKey.hostUrl.value != null) {
debugPrint(" ==== Success Url");
loadNewUrl(onDone);
}
}
static Future<void> checkResolverConfig(context) async {
List<String?> resolverListEncode = await getConfigFromResolverList();
if(resolverListEncode.isEmpty) {
return;
}
String? newEncodedConfig = getMergedConfig(resolverListEncode: resolverListEncode);
await replaceConfig(newEncodedConfig: newEncodedConfig);
}
static String? getMergedConfig({required List<String?> resolverListEncode}) {
if (resolverListEncode[0] != null && resolverListEncode[1] != null && resolverListEncode[0] == resolverListEncode[1]) {
return resolverListEncode[1]!;
}
if (resolverListEncode[0] != null && resolverListEncode[1] != null && resolverListEncode[0] != resolverListEncode[1]) {
return null;
}
if (resolverListEncode[0] != null && resolverListEncode[1] == null) {
return resolverListEncode[0]!;
}
if (resolverListEncode[0] == null && resolverListEncode[1] != null) {
return resolverListEncode[1]!;
}
if (resolverListEncode[0] == null && resolverListEncode[1] == null) {
return null;
}
return null;
}
static Future<void> replaceConfig({String? newEncodedConfig}) async {
if (newEncodedConfig != null) {
String newDecodedConfig = Utils.decrypt(newEncodedConfig);
Map newDecodedConfigJson = json.decode(Uri.decodeFull(newDecodedConfig));
Constants.defaultHostUrl = newDecodedConfigJson['serviceUrl'];
RenderKey.hostUrl.value = newDecodedConfigJson['serviceUrl'];
RenderKey.urlEditController.text = RenderKey.hostUrl.value!;
return;
}
RenderKey.hostUrl.value = Constants.config.defaultHost[Constants.packageInfo.packageName]!;
}
/// 获取线上域名密文JSON信息
static Future<List<String?>> getConfigFromResolverList() async {
List<String?> resolverEncode = [];
await Future.forEach(Constants.resolverList, (resolver) async {
debugPrint("resolver $resolver");
try {
Response response = await DioUtil.dioClient.get("$resolver?random=${Random().nextInt(9999)}");
debugPrint("response: ${response.data}");
resolverEncode.add(response.data['token']);
} catch (error) {
resolverEncode.add(null);
}
});
return resolverEncode;
}
多应用配置初始化
在 /jianghuAppBrowser/jianghuBrowser/lib/layout/JianghuConfigHandler.dart
的init
方法中,对多应用配置做了处理。
Future<void> init(JianghuConfigEntity config) async {
Constants.config = config;
if (!Constants.isBrowser) Constants.appId.value = config.appIds[Constants.packageInfo.packageName]!;
if (!Constants.isBrowser) Constants.defaultAppId = Constants.appId.value;
openBottomBar = config.openBottomBar[Constants.packageInfo.packageName]!;
backgroundNotice = config.backgroundNotice[Constants.packageInfo.packageName]!;
openSocket = config.openSocket[Constants.packageInfo.packageName]!;
setPrimaryColor();
if (!Constants.isBrowser) Constants.resolverListEnv = config.resolverJsons;
Constants.appName = config.appName[Constants.packageInfo.packageName]!;
}
void setPrimaryColor() {
if (Constants.config.primaryColor != null) {
Constants.primaryColor = (Constants.config.primaryColor![Constants.packageInfo.packageName] ?? const Color(0xFFFFFFFF));
} else {
Constants.primaryColor = const Color(0xFFFFFFFF);
}
}
安卓端的多应用配置
可以在Flutter项目中为不同的安卓应用程序定义不同的配置。
图标的配置
- 图标的生成
可以访问 图标工厂,上传jpg/png/psd文件,即可同时生成 iOS、Android 和 PhoneGap
应用的图标。遵循 Apple、 Google 官方标准。支持自定义大小、自动圆角、角标和徽章等。支持 Web App, iWatch 等更多平台。快速预览将要在不同设备上显示的应用图标,无需部署即可通过预览来调整设计样式。
- 图标的使用
将生成的图标复制到 /jianghuAppBrowser/jianghuBrowser/android/app/src/main/res
目录下,并在 /jianghuAppBrowser/jianghuBrowser/android/app/build.gradle
中配置图标路径。具体配置方法请参考下文。
应用参数配置
应用参数在 /jianghuAppBrowser/jianghuBrowser/android/app/build.gradle
中配置。
android.defaultConfig
定义默认配置,android.productFlavors
定义多应用配置。如果要开启多应用配置,需要在android.defaultConfig.flavorDimensions
中开启。android.productFlavors.dimension
的值要与android.defaultConfig.flavorDimensions
的值一致。如果不配置android.defaultConfig.flavorDimensions
,则默认不开启多应用模式,android.productFlavors
中的配置无效,会默认使用android.defaultConfig
的配置。
默认配置:
android {
defaultConfig {
applicationId "org.fsll.jianghu_browser"
minSdkVersion 24
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
flavorDimensions 'jianghu_browser' //开启多应用配置
}
}
多应用配置:
android {
//应用签名配置
signingConfigs {
browser {
storeFile file('./browser.jks')
storePassword '123456'
keyAlias 'browser'
keyPassword '123456'
}
duoxing {
storeFile file('./duoxing5.jks')
storePassword '123456'
keyAlias 'duoxing5'
keyPassword '123456'
}
jianghu3 {
storeFile file('./jianghu3.jks')
storePassword '123456'
keyAlias 'jianghu3'
keyPassword '123456'
}
},
//多应用配置
productFlavors {
browser {
applicationId "org.fsll.browser"
resValue "string", "app_name", "江湖浏览器"
resValue "string", "icon", "@mipmap/browser_launcher" //图标在/jianghuAppBrowser/jianghuBrowser/android/app/src/main/res目录下的mipmap-xxx
resValue "string", "service_name", "browser_service"
resValue "string", "screen", "@drawable/screen" //启动屏图片,在/jianghuAppBrowser/jianghuBrowser/android/app/src/main/res目录下的drawable-xxx
resValue "color", "primary", "#FFFFFFFF"
buildConfigField "boolean", "SERVER_DEBUG", 'false'
dimension "jianghu_browser"
signingConfig signingConfigs.browser //签名,在以上signingConfigs中配置
}
duoxing {
applicationId "org.fsll.duoxing5"
resValue "string", "app_name", "多星5"
resValue "string", "icon", "@mipmap/dx5_launcher"
resValue "string", "service_name", "duoxing_service"
resValue "string", "screen", "@drawable/screen"
resValue "color", "primary", "#FF00579c"
buildConfigField "boolean", "SERVER_DEBUG", 'false'
dimension "jianghu_browser"
signingConfig signingConfigs.duoxing
}
jianghu3 {
applicationId "org.fsll.jianghu3"
resValue "string", "app_name", "江湖3"
resValue "string", "icon", "@mipmap/jianghu_launcher"
resValue "string", "service_name", "jianghu_service"
resValue "string", "screen", "@drawable/screen"
resValue "color", "primary", "#FFff5300"
buildConfigField "boolean", "SERVER_DEBUG", 'false'
dimension "jianghu_browser"
signingConfig signingConfigs.jianghu3
}
}
}
签名的相关说明可以参考 生成签名。
IOS的多应用配置
图标的配置
- 图标的生成
可以访问 图标工厂,上传jpg/png/psd文件,即可同时生成 iOS、Android 和 PhoneGap
应用的图标。遵循 Apple、 Google 官方标准。支持自定义大小、自动圆角、角标和徽章等。支持 Web App, iWatch 等更多平台。快速预览将要在不同设备上显示的应用图标,无需部署即可通过预览来调整设计样式。
图标的使用
target的配置
Duplicate target的项目,名字不要空格
新建Assets.xcassets,重命名,放入新图标,右键 new Color set 设置专用颜色
新建LaunchScreen.storyboard,重命名,view设置不同颜色
选择taget目标, General中设置ID,图标,启动图
在AppDelegate.swift中,根据对应的app ID,配置对应的背景颜色
在Ios->Runner->AppDelegate.swift->application中添加代码
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
setAppBackgroundColor(forBundleIdentifier: "org.fsll.duoxing5", withColorName: "duoxingColor")
...
}
func setAppBackgroundColor(forBundleIdentifier bundleIdentifier: String, withColorName colorName: String) {
if Bundle.main.bundleIdentifier == bundleIdentifier, let color = UIColor(named: colorName) {
window?.backgroundColor = color
}
}
切换到Build settings,搜索info.plist,地址更换为新生成的新的 Info.plist
选择Target目标,点击左下角+, 添加 Share-Extension,配置Share-Extension的info.plist,配置ShareViewController的代码
在General的Framework、Libraries、and Embedded content,点击+,选择专属的Share-Extension,
配置专属的scheme,一般会自动生成,没有就手动新加个,相关选项,选择对应的新的target就行
选择新的scheme,选择设备启动
在product的archive,生成新的包发到app store就可以了
要在flutter中能正常使用flover启动,还需如下配置
在podfile中,配置target 新 target NewTargetName do名字,直接复制原来的target Runner do就可以,几个target,就复制几次,每个target名字都配置上,project Runner不要动
命令行导航到项目的iOS目录,先后运行:rm -rf Pods Podfile.lock,pod deintegrate,pod install
在iOS/Flutter目录中复制Debug.xcconfig, release同样,修改名字为(Debug | Relase)-NewTargetName.xcconfig,修改其内容的Pods-Runner -> Pods-NewTargetName
在Xcode中,选择 project runner -> info,选择configurations,复制(Debug、Release)为新的(Debug | Release)-NewTargetName,
展开新的配置(Debug | Release)-NewTargetName,选择对应的NewTargetName,选择他的Based On Config File为对应的xcconfig,只需要修改自身对应的Target即可,其他的不用管,默认即可
在Xcode,选择修改scheme,修改相关的config的地方为新的config配置
在Android Studio,项目中的Edit Configuration中,启动参数添加--flavor=NewTargetName
成功后,flutter端启动app即可
多包分配后可能出现的Bug
- xcode rsync error: some files could not be transferred (code 23) at main.c
- XCode14.X, 编译(build)正常,打包(Archive)出错
PhaseScriptExecution [CP]\ Embed\ Pods\ Frameworks /Users/zhangzheng/Library/Developer/Xcode/DerivedData/OWON-abukyxpajbueubdempshornbemjw/Build/Intermediates.noindex/ArchiveIntermediates/OWON/IntermediateBuildFilesPath/OWON.build/Release-iphoneos/OWON.build/Script-361D99D13FFD29F042D080E7.sh (in target 'OWON' from project 'OWON')
sent 29 bytes received 20 bytes 98.00 bytes/sec
total size is 0 speedup is 0.00
rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/97f6331a-ba75-11ed-a4bc-863efbbaf80d/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996) [sender=2.6.9]
Command PhaseScriptExecution failed with a nonzero exit code
解决办法
- 在Pods/Targets Support Files/Pods-NewTargetName/Pods-NewTargetName-frameworks.sh中找到代码 source="$(readlink "${source}")"
- 替换成source="$(readlink -f "${source}")", // 注意-f后边有个空格
- 重新Archive打包,就成功了
模拟器上就是不出现在系统分享列表里
- 在主程序target -> Build Phases -> Embed Foundation Extensions下,取消勾选Copy only when installing