面向多 App(多品牌/多场景)孵化的,基于插件化架构的Flutter项目脚手架(模板),非常适合矩阵包形式,让你快速通过插件化开发Flutter项目
该仓库核心目标:
- 在同一套基础设施下快速孵化新 App Shell(矩阵包)
- 通过“模块包”组合业务能力,而不是复制整套工程
- 在模块内部应用 Clean Architecture,模块之间保持低耦合
- 品牌差异(不同的app)通过配置驱动,而不是硬编码在业务逻辑中
本仓库遵循以下约束:
- 使用模块化架构
- 每个业务模块是独立 package
- Clean Architecture 仅在模块内部生效
- 不做全局 data/domain/presentation 大一统拆分
- 模块低耦合,跨模块通过契约/能力交互
shared只放真正共享的抽象,不放无关代码
- Flutter
- melos(Monorepo 编排)
- dio(网络基础能力)
- get_it(依赖注入)
- get(路由 + 状态管理)
- injectable(已纳入技术栈约束,可按需引入)
.
├── apps/
│ ├── app_consumer/
│ └── app_doctor/
├── packages/
│ ├── foundation/ # 启动编排、模块协议、配置模型、DI
│ ├── brands/ # 品牌/环境配置生成
│ ├── design_system/ # 主题与通用 UI 组件
│ ├── shared/ # 跨模块契约与共享路由
│ └── modules/
│ ├── module_auth/
│ ├── module_home/
│ └── module_profile/
└── tooling/ # 脚手架/契约校验/IDE 修复脚本
说明:
apps/和packages/modules/下当前列出的目录仅用于示例说明,可根据实际项目进行新增、删减或替换。
⚠️ apps/和packages/modules/下当前列出的目录仅用于示例说明,可根据实际项目进行app和module的新增、删减或替换。
以 apps/app_consumer/lib/main.dart 为例,启动流程如下:
- 读取品牌配置:
BrandProfiles.consumer(env: AppEnv.dev) - 组装模块目录:
moduleCatalog = {'module_auth': ..., ...} - 根据配置启用模块:
ModuleSelector.select(...) - 启动编排:
AppBootstrapper.bootstrap(config, modules) - 拿到注册结果:
ModuleRegistry(包含路由、模块元信息) - 构建
GetMaterialApp,注入模块路由和调试页/debug/modules
这条链路实现了“App 壳 + 模块插件化装配”的分层职责。
位于 packages/foundation/lib/src/app_bootstrap/app_module.dart。
模块必须声明:
moduleName:模块唯一名descriptor:版本、依赖、能力声明pages:模块路由列表registerDependencies(GetIt sl, AppConfig config):模块内依赖注册initialize(AppConfig config):可选异步初始化
位于 packages/foundation/lib/src/app_bootstrap/module_descriptor.dart。
ModuleDependency:声明依赖模块及版本约束ModuleDescriptor:模块版本、依赖、对外能力
ModuleRegistry._validateDependencies() 会在启动阶段校验依赖完整性,缺依赖直接抛错,防止运行期隐式失败。
位于 packages/foundation/lib/src/app_bootstrap/module_registry.dart。
职责:
- 启动前依赖校验
- 逐模块调用
registerDependencies与initialize - 汇总模块路由
collectPages() - 汇总模块名/描述用于调试页展示
- 协同能力注册(见下)
位于:
packages/foundation/lib/src/app_bootstrap/capability/module_capability_provider.dartpackages/foundation/lib/src/app_bootstrap/capability/capability_registry.dart
能力机制用于跨模块“面向接口”通信:
- 提供方模块实现
ModuleCapabilityProvider并registry.register<T>(impl) - 使用方模块通过
CapabilityRegistry.require<T>()获取能力 - 若重复注册相同 capability type,系统会记录 warning 并忽略后续重复
这样的目的是:避免模块之间直接 import 彼此实现,降低耦合。
位于 packages/foundation/lib/src/app_bootstrap/app_bootstrapper.dart。
负责:
- 重置并初始化
serviceLocator - 注册核心对象(
AppConfig、Dio) - 创建并注册
CapabilityRegistry - 创建并执行
ModuleRegistry.registerAll(...)
配置模型位于 packages/foundation/lib/src/app_config/。
聚合应用运行所需的全部配置:
appNameenvapiConfigfeatureFlagsanalyticsConfigbrandConfigenabledModules
使用方式:
- 启动时由
BrandProfiles.xxx(env: ...)生成 - 模块在
registerDependencies或 Controller 中通过sl<AppConfig>()/ 构造参数读取
作用:定义 API Base URL 和连接超时。
使用方式:
AppBootstrapper用它初始化全局Dio(BaseOptions(...))
作用:按品牌控制功能开关。
使用方式:
- 业务用例读取开关并决定是否产出功能数据(如
LoadHomeEntriesUseCase)
作用:埋点开关和埋点提供方。
使用方式:
- 页面/用例可读取 provider 做分流策略
作用:品牌标识、品牌名称、主题色。
使用方式:
AppThemeFactory.light(config.brandConfig)生成主题
作用:定义 dev/staging/prod 环境。
使用方式:
BrandProfiles根据环境切换 API 域名
核心文件:packages/brands/lib/src/brand_profiles.dart
当前已提供:
BrandProfiles.consumer(...)BrandProfiles.doctor(...)
每个 profile 同时决定:
- app 名称
- brand 主题色
- analytics provider
- feature flags
- enabledModules
- 不同 env 的 API 域名
这使“品牌差异”集中在配置层,而业务模块代码保持稳定。
作用:
- 提供登录页面
/auth/login - 注册
AuthRepository、LoginUseCase、AuthSessionService - 对外暴露能力:
SessionReadable
用法:
- App 将
ModuleAuth()放入moduleCatalog - 若
enabledModules含module_auth,则自动加载对应路由和依赖
协议说明(JSON + XML 混用):
module_auth支持“同模块混合协议”:- 登录接口可走 XML(历史遗留接口)。
- 其余接口继续走 JSON。
- 协议差异仅在
data层处理,domain/presentation不感知 XML/JSON。 - 当前通过
FeatureFlags.useXmlLoginApi控制登录是否走 XML:false:走原有 JSON 登录链路。true:走 XML 登录链路(dio + xml2json + LoginXmlParser)。
作用:
- 提供首页
/home - 依赖
module_auth(声明式依赖,拒绝直接依赖) - 根据
AppConfig和FeatureFlags生成首页条目
用法:
ModuleHome()加入目录并启用- Controller 在
onInit调用LoadHomeEntriesUseCase
作用:
- 提供资料(个人中心)页
/profile - 依赖
module_auth(声明式依赖,拒绝直接依赖) - 对外暴露能力:
ProfileReadable
用法:
ModuleProfile()加入目录并启用- 通过
LoadProfileUseCase聚合品牌/埋点/功能统计
AppRoutes:跨模块统一路由常量SessionReadable/ProfileReadable:跨模块能力契约ContractVersions:契约版本号
相关脚本:
tooling/check_contract_compatibility.shtooling/update_contract_lock.shtooling/contracts_manifest.txttooling/contracts.lock
规则:
- 接口文件哈希变化但 major 未升级 -> 校验失败
- 版本变化但接口哈希未变化 -> 校验失败
推荐流程:
- 修改契约接口
- 同步更新
ContractVersions(必要时 major) - 执行
./tooling/check_contract_compatibility.sh - 确认后执行
./tooling/update_contract_lock.sh
melos bootstrapcd apps/app_consumer
flutter run
# 或
cd apps/app_doctor
flutter runmelos run contract:check
melos run analyze
melos run test一键 CI 本地预演:
melos run ci下面给出一个“新增孵化 App:app_pharmacy”的完整示例,新增更多app:app_d、app_e、app_f。。。同理。
./tooling/create_app_shell.sh app_pharmacy consumer dev说明:
- 参数 1:新 App 名(会创建
apps/app_pharmacy) - 参数 2:品牌模板(当前脚本默认支持
consumer/doctor) - 参数 3:环境(可选,默认
dev)
然后:
cd apps/app_pharmacy
flutter pub get
flutter run脚手架自动生成:
- app 级
main.dart(含BrandProfiles + ModuleSelector + AppBootstrapper) - 认证/首页/资料模块依赖
- 模块调试页路由接入
若要“药房品牌”而不是复用 consumer/doctor,可先在 BrandProfiles 增加 pharmacy:
static AppConfig pharmacy({required AppEnv env}) {
return AppConfig(
appName: 'Pharmacy App',
env: env,
apiConfig: ApiConfig(baseUrl: _apiByEnv(env, consumer: true)),
analyticsConfig: const AnalyticsConfig(enabled: true, provider: 'firebase'),
brandConfig: const BrandConfig(
brandId: 'pharmacy',
brandName: 'Life Pharmacy',
seedColor: Color(0xFF5B8C00),
),
featureFlags: const FeatureFlags(
enablePayment: true,
enableSubscription: false,
enableMedication: true,
enableDietarySupplements: false,
),
enabledModules: const <String>[
'module_auth',
'module_home',
'module_profile',
],
);
}然后在新 App main.dart 中使用:
final config = BrandProfiles.pharmacy(env: AppEnv.staging);final moduleCatalog = <String, AppModule>{
'module_auth': ModuleAuth(),
'module_home': ModuleHome(),
'module_profile': ModuleProfile(),
// 'module_orders': ModuleOrders(), // 新模块按需接入
};
final modules = ModuleSelector.select(
catalog: moduleCatalog,
enabledModules: config.enabledModules,
);
final registry = await AppBootstrapper.bootstrap(
config: config,
modules: modules,
);要点:
moduleCatalog代表“App 可安装插件全集”enabledModules代表“当前品牌/环境启用集”- 两者交集即运行时模块集
melos bootstrap
melos run analyze
melos run test并手动启动新 App 验证:
- 登录页可进入
- 首页展示正常
/debug/modules可看到模块、路由、能力、告警
./tooling/create_module.sh module_orders会自动生成:
module_orders.dart对外导出src/module_orders_module.dart(实现AppModule)- application/domain/presentation 基础目录
- 在 App
pubspec.yaml添加 path 依赖:
module_orders:
path: ../../packages/modules/module_orders- 在 App
main.dart添加 import 与 catalog:
import 'package:module_orders/module_orders.dart';
final moduleCatalog = <String, AppModule>{
'module_auth': ModuleAuth(),
'module_home': ModuleHome(),
'module_profile': ModuleProfile(),
'module_orders': ModuleOrders(),
};- 在品牌配置
enabledModules中按需启用:
enabledModules: const <String>[
'module_auth',
'module_home',
'module_orders',
],在模块 descriptor 里声明:
ModuleDescriptor get descriptor => const ModuleDescriptor(
moduleName: 'module_orders',
version: '0.1.0',
dependencies: <ModuleDependency>[
ModuleDependency(moduleName: 'module_auth', versionConstraint: '>=0.1.0'),
],
);若 App 启用了 module_orders 却没启用 module_auth,启动时会被 ModuleRegistry 拦截并抛错。
module_profile 示例:
class ModuleProfile implements AppModule, ModuleCapabilityProvider {
@override
void registerCapabilities(
CapabilityRegistry registry,
GetIt serviceLocator,
AppConfig config,
) {
registry.register<ProfileReadable>(serviceLocator<ProfileReadService>());
}
}示例(伪代码):
final capabilityRegistry = serviceLocator<CapabilityRegistry>();
final profileReadable = capabilityRegistry.tryGet<ProfileReadable>();
if (profileReadable != null) {
final name = await profileReadable.displayName();
// 使用能力结果
}建议:
- 优先
tryGet<T>()做可选能力,避免强依赖 - 必须依赖时用
require<T>()并在模块依赖里显式声明前置模块
路由:/debug/modules
可查看:
- 当前 App 配置
- 已启用模块
- 模块描述与依赖
- 注册路由
- 已注册 capability
- 启动告警(含重复 capability 注册)
- 启动时报模块依赖缺失
- 检查品牌
enabledModules - 检查模块
descriptor.dependencies
- 路由找不到
- 检查模块是否被启用
- 检查模块
pages是否正确暴露
- capability 获取失败
- 检查提供方是否实现
ModuleCapabilityProvider - 检查是否在
registerCapabilities注册
- Android Studio 运行配置错乱
./tooling/repair_flutter_ide.shmelos bootstrap
melos run analyze
melos run test
melos run format
melos run gen
melos run contract:check
melos run contract:update-lock
melos run scaffold:app
melos run scaffold:module
melos run repair:ide
melos run ci- 优先最小可运行实现,再逐步抽象
- 模块边界清晰优先于“公共代码复用率”
- 共享包只放稳定契约/通用能力
- 品牌差异统一放
brands配置层 - 每次变更前后执行:导入检查、依赖关系检查、双 Demo App 启动验证
- 全局
Dio由foundation统一注册与注入(AppBootstrapper -> DioClientFactory)。 AuthInterceptor统一处理:- 请求前自动注入
Authorization。 401自动触发 refresh。- refresh 成功后重放原请求。
- refresh 失败后清 token 并调用
UnauthorizedHandler(如跳转登录页)。
- 请求前自动注入
- 业务模块只依赖注入后的
Dio/AuthApi,不在模块里重复写 token 失效逻辑。
import 'package:json_annotation/json_annotation.dart';
part 'test_dto.g.dart';
@JsonSerializable()
class TestDto {
const TtDto(this.a);
final String a;
factory TestDto.fromJson(Map<String, dynamic> json) => _$TestDtoFromJson(json);
Map<String, dynamic> toJson() => _$TestDtoToJson(this);
}注意:
part 'xxx.g.dart';必须与文件名一一对应。fromJson/toJson可以先写声明,生成函数由build_runner写入*.g.dart。
melos run gen说明:
melos run gen会调用./tooling/run_codegen.sh。- 脚本会扫描
apps/、packages/下含build_runner的包并执行生成。 - 脚本兼容无
rg环境(会自动回退到find)。
RUNNING (in 0 packages)
- 说明当前 melos 过滤未命中包;优先使用仓库内
melos run gen(已内置扫描逻辑)。
xxx.g.dart must be included as a part directive
- 在源文件补上
part 'xxx.g.dart';,并确保与文件名一致。
- 生成后 IDE 仍不跳转定义
- 执行
melos run repair:ide,必要时Invalidate Caches / Restart。
可在 main.dart 的启动编排处传入实现:
final registry = await AppBootstrapper.bootstrap(
config: config,
modules: modules,
tokenStore: MyTokenStore(), // 建议接 secure storage
authRefresher: MyAuthRefresher(), // 调用 refresh token 接口
unauthorizedHandler: MyUnauthorizedHandler(), // 清会话并跳登录
enableAutoRefresh: true,
);接口说明(均在 foundation 导出):
TokenStore:读写accessToken/refreshTokenAuthRefresher:401时刷新 tokenUnauthorizedHandler:刷新失败后的全局收敛动作
如果暂未接入真实实现,可不传,系统默认使用 SecureTokenStore + NoopAuthRefresher + NoopUnauthorizedHandler:
- token 会写入系统安全存储(Keychain/Keystore)。
- 未接入真实 refresh 逻辑时,
401不会刷新成功,但应用不会崩溃。 - 测试场景可显式传入
DefaultTokenStore()覆盖默认实现。
当前 DioClientFactory 的拦截器顺序:
AuthInterceptorLoggingInterceptor(仅非prod环境)extraInterceptors(可根据你的业务按需追加)
AuthInterceptor 关键行为:
onRequest:从TokenStore读取 token 并注入Authorization。onError:命中401且未重试时触发 refresh。- refresh 成功:更新 token 并重放原请求。
- refresh 失败:清理 token 并调用
UnauthorizedHandler。
防重试死循环:
- 内部使用
requestOptions.extra['__retry_after_refresh__'] = true标记重放请求。 - 已带该标记的请求不会再次触发 refresh。
跳过 refresh(例如 refresh 接口本身):
- 对特定请求设置
requestOptions.extra['__skip_refresh__'] = true。 - 设置后即使返回
401也不会进入自动刷新逻辑。
适用场景:
- 模块内接口是 JSON,还有少量历史接口是 XML,如:登陆接口是 XML。
举个例子-假设登陆接口是XML:
- 登录接口支持 XML 分支(
AuthRemoteDataSource._loginByXml)。 - XML 响应通过
LoginXmlParser(xml2json)解析为LoginResponseDto。 - 代码中已明确标注
TODO,提示当前 URL/字段为临时占位。
如何启用 XML 调用登陆接口:
- 在品牌配置中将
FeatureFlags.useXmlLoginApi设为true。 - 保持其他接口继续使用 JSON API(Retrofit)不变。
该项目中的XML部分是mock数据,你使用时必须替换为你真实接口字段!!!
⚠️ 该项目中的XML部分是mock数据,你使用时必须替换为你真实接口字段!!!
- 替换 XML 请求地址:
- 不使用临时
'/auth/login'。 - 按真实后端路由替换(示例:
aaa/bbb/login)。
- 不使用临时
- 替换 XML 请求体结构:
- 不使用临时
<login><account>...。 - 按真实
PhoneLoginOnPack字段构造(如PhoneNumber、PassWord、DeviceID、IsValid等)。
- 不使用临时
- 替换 XML 响应解析字段:
- 不使用通用候选字段(
success/code/msg)。 - 按真实字段解析与判定(如
ReturnFlag、ReturnText、MemberID、PortPassword)。 - 成功判定建议遵循真实协议规则(如
ReturnFlag以#SUCCESS#开头)。
- 不使用通用候选字段(
- 使用真实报文回归:
- 至少覆盖:登录成功、账号/密码错误、风控/封禁、字段缺失。