✨ 主打分层架构 · MVVM · Clean Architecture 的 Flutter 开箱即用代码模板
这是一个面向中大型 Flutter 项目的快速启动模板。项目以 Feature-First 组织业务模块,在每个业务 Feature 内落地 Data / Domain / Presentation 分层,并通过 BasePage + PageLogic + BaseVM / BaseAutoDisposeVM + Riverpod 建立职责清晰的 MVVM 开发范式。
模板已内置多环境、网络请求、路由守卫、主题、国际化、本地存储、登录会话、通用 WebView 页面、Toast/Loading、刷新、按钮、弹窗等常用基础设施。Clone 后只需要替换业务接口与页面,即可进入功能开发。
📘 第一次使用模板请先阅读:模板使用说明。
🧪 测试与质量门禁请查看:测试与质量保障。
🤖 AI 指令统一管理请查看:Ruler AI 指令管理。
| 能力 | 说明 |
|---|---|
| 清晰分层 | 每个业务模块按 data / domain / presentation 拆分,职责边界明确 |
| MVVM 开发范式 | BasePage 承载页面骨架,PageLogic 承载页面私有生命周期与局部交互,BaseVM / BaseAutoDisposeVM 只承载状态与业务动作 |
| Clean Architecture | 业务依赖抽象而非实现,Repository 接口与实现分离,便于替换数据源 |
| Feature-First | 业务代码按功能聚合,模块可独立演进,避免按技术层级散落全局 |
| Riverpod 驱动 | 状态管理、依赖注入、服务组合统一使用 Riverpod,减少框架混用成本 |
| 开箱即用基础设施 | 网络、路由、主题、存储、认证、国际化、异常捕获、日志、组件均已预置 |
| 多环境支持 | Dev / SIT / Prod 独立入口,环境配置通过 EnvConfig 注入 |
| 易扩展 | 新增 Feature 时复用既有目录、路由、ViewModel、Repository 模式即可 |
# 安装依赖
flutter pub get
# 运行开发环境
flutter run -t lib/main_dev.dart
# 运行 SIT 环境
flutter run -t lib/main_sit.dart
# 运行生产环境
flutter run -t lib/main_prod.dart
# 默认入口同样指向生产环境
flutter run -t lib/main.dart首次使用建议先修改各环境入口中的 EnvConfig:
await Application.run(
envConfig: const EnvConfig(
envTag: EnvTag.dev,
baseUrl: 'https://dev.example.com',
apiPathPrefix: '/api',
),
);lib/
├── app/ # 应用启动、根组件、App 路由装配
│ ├── app.dart # MaterialApp.router 根组件
│ ├── application.dart # Application.run 启动入口
│ ├── host/ # 启动协调、会话协调、AppHost
│ ├── navigation/ # App 路由图组合、Splash、Root/Shell 导航骨架
│ │ ├── app_router_config.dart # AppRouterConfig 注入
│ │ ├── shell/ # RootRoute / RootShellRoute / 底部 Tab 容器
│ │ └── splash/ # 启动展示页
├── core/ # 全局基础设施,不承载具体业务
│ ├── config/ # EnvConfig / EnvTag / appConfig
│ ├── constant/ # 常量
│ ├── exception/ # 全局异常捕获
│ ├── feature/ # AppFeature 模块协议
│ ├── l10n/ # 国际化
│ ├── network/ # 网络、连接状态、HTTP 客户端
│ ├── router/ # GoRouter 封装、守卫、导航抽象
│ ├── service/ # 日志等全局基础服务
│ ├── storage/ # SharedPreferences / SecureStorage 封装
│ ├── theme/ # 主题、色值、主题资源
│ └── util/ # 工具类
├── features/ # 业务功能模块
│ ├── exports.dart # 业务页面可用的 Route / 公开类型导出
│ ├── features.dart # App Feature 注册与路由/Tab/Provider 汇聚
│ ├── auth/ # 登录示例
│ ├── profile/ # 个人中心与主题/退出登录示例
│ └── todo/ # 默认根 Tab 与完整分层示例
├── shared/ # 跨 Feature 共享能力
│ ├── presentation/ # BasePage / PageLogic / BaseVM / BaseAutoDisposeVM / BaseState / PresentationFeedbackService
│ ├── services/ # AuthSession / AuthStore / AppEventBus
│ ├── widgets/ # Toast、Loading、Button、Dialog 等组件
│ └── webview/ # 通用 WebView 页面、配置与公共路由
├── header.dart # 业务页面常用导出,app/core/shared 内部不使用
├── main_dev.dart # 开发环境入口
├── main_sit.dart # SIT 环境入口
├── main_prod.dart # 生产环境入口
└── main.dart # 默认入口
项目根目录还包含 AI 指令管理源文件:
.ruler/ # Ruler AI 指令源文件,需纳入版本控制
├── AGENTS.md # 项目 AI 指令总入口
├── ruler.toml # Ruler 配置
├── architecture.md # 架构规则
├── flutter_conventions.md # Flutter / Dart 约定
├── routing.md # 路由规则
├── state_management.md # 状态管理规则
├── testing.md # 测试与质量规则
├── documentation.md # 文档维护规则
└── skills/ # 可分发给 AI 工具的项目技能
Ruler 生成的 AGENTS.md、CLAUDE.md、.claude/skills/、.cursor/skills/、.codex/ 等文件由 .gitignore 忽略,不应手动修改或提交。详见 Ruler AI 指令管理。
单个 Feature 推荐结构:
lib/features/<feature>/
├── <feature>_feature.dart # Feature 声明与路由导出
├── data/
│ ├── datasources/ # 远程/本地数据源
│ └── repositories/ # Repository 实现
├── domain/
│ ├── entities/ # 业务实体
│ ├── exceptions/ # 领域异常
│ └── repositories/ # Repository 抽象
└── presentation/
├── pages/ # 页面
├── viewmodels/ # ViewModel
└── <feature>_routes.dart # Feature 路由定义
该结构适用于 Todo、订单、登录等有业务状态、Repository 或接口编排的复杂 Feature。需要把 Repository 返回结果写入 App 级状态源时,优先通过 shared/services 中的稳定 Controller 或 Service 编排。纯展示页、设置页、Profile 这类只读取少量全局 Provider 或只有简单点击回调的页面,可以只保留 presentation/pages 与路由文件,不需要为了形式统一创建空 ViewModel、空 State、空 data 或空 domain 目录。
┌──────────────────────────────────────────────┐
│ Presentation │
│ Page · ViewModel · Route · Widget │
│ UI 展示、状态管理、交互 │
├──────────────────────────────────────────────┤
│ Domain │
│ Entity · Repository 接口 │
│ 业务模型、业务抽象 │
├──────────────────────────────────────────────┤
│ Data │
│ DataSource · Repository 实现 │
│ 接口请求、缓存、数据转换 │
└──────────────────────────────────────────────┘
依赖方向:
Presentation ──────▶ Domain ◀────── Data
UI 抽象 实现
这种依赖关系让页面只关心业务抽象,数据来源可以在 API、Mock、本地缓存之间切换,而不会影响 UI 层。
| 模块 | 职责 | 依赖规则 |
|---|---|---|
app/ |
应用启动、环境注入、根组件挂载 | 可组合全局能力 |
core/ |
网络、路由、存储、主题、DI、异常、工具 | 不依赖具体 Feature |
features/ |
业务模块 | 可依赖 core 与 shared |
shared/ |
BasePage、PageLogic、BaseVM、BaseAutoDisposeVM、PresentationFeedbackService、认证服务、事件总线、通用组件、通用 WebView 等跨 Feature 能力 | 提供跨业务复用能力,不承载具体业务流程 |
模板通过 BasePage、PageLogic、BaseVM、BaseAutoDisposeVM、BaseState、PresentationFeedbackService 固化页面开发方式,同时保持 Page、PageLogic 与 ViewModel 的职责边界:Page 负责 UI 结构、Widget 组合、布局、样式;PageLogic 负责页面本地 controller、临时交互状态、生命周期、调用 VM/Provider;ViewModel / Notifier 负责页面可观察状态、业务动作编排,并把领域/服务状态转换成 UI 状态。
BasePage 是纯页面壳,不绑定具体 ViewModel,负责 UI 结构、Widget 组合、布局和样式:
- 标准
Scaffold构建 - AppBar、背景色、SafeArea、KeepAlive 扩展点
- 背景点击自动收起键盘
- App 生命周期监听与
PageLogic生命周期分发:onInit/onReady/onResume/onPause/onDispose - Pop 拦截与默认返回能力
- 通过
PageScope向子类提供context、ref与可选PageLogic
PageLogic 是按需使用的页面本地逻辑层,用于承载只属于当前页面的 controller、FocusNode、临时交互状态、生命周期、首帧副作用,以及页面级导航/弹窗编排,不放跨页面业务状态。页面如需使用,覆盖 createPageLogic() 并通过 scope.logic<XxxPageLogic>() 取得实例:
final class OrderPageLogic extends PageLogic {
@override
void onReady() {
unawaited(loadOrders());
}
Future<void> loadOrders() {
return presentation.runWithLoading(
() => ref.read(orderViewModelProvider.notifier).loadOrders(),
rethrowError: false,
);
}
}推荐使用 PageLogic 的场景:
- 页面持有
TextEditingController、FocusNode、ScrollController或动画控制器。 - 页面需要
onReady、onResume、onPause、首帧加载或可见性相关副作用。 - 页面存在不适合进入 ViewModel 的临时 UI 状态。
- 页面需要编排登录后跳转、确认弹窗、输入框提交等 UI 副作用。
可以不使用 PageLogic 的场景:
- 纯展示页面。
- 只通过
ref.watch渲染 Provider 状态的简单页面。 - 只有少量点击回调,且不需要 controller、生命周期或页面私有状态。
PageLogic 可以调用 VM/Provider,但不要承载可观察业务状态、接口编排、跨页面状态或领域逻辑;这些职责应放入 ViewModel、Service、Repository 或稳定 Provider。
页面在 page(scope) 中按需桥接状态与 PageLogic:
@override
Widget page(PageScope scope) {
final logic = scope.logic<OrderPageLogic>();
final state = scope.ref.watch(todoViewModelProvider);
return TodoContent(state: state, onRefresh: logic.loadOrders);
}BaseVM 与 BaseAutoDisposeVM 分别基于 Riverpod Notifier 与 AutoDisposeNotifier,只用于承载页面可观察状态、业务动作编排,并把领域/服务状态转换成 UI 状态:
initialState()提供初始状态- 通过
state = state.copyWith(...)更新 UI 状态 - 调用 Repository 抽象完成业务数据读写
- 不感知
BuildContext、Widget 生命周期、页面返回、页面 ready 策略或一次性 UI 反馈服务
选择基类时按状态保留需求区分:需要跨页面或离开页面后继续保留状态时使用 BaseVM 并配套 NotifierProvider;只服务当前页面、最后一个监听者移除后即可释放状态时使用 BaseAutoDisposeVM 并配套 AutoDisposeNotifierProvider。
BaseState 是纯状态基类,不内置页面级字段。业务页面如需首屏加载、空态、错误态,应在各自 Feature 的 State 中显式建模,例如 initialized、loading、errorMessage。
PresentationFeedbackService 承接 Presentation 层的一次性 UI 反馈,并通过 presentationFeedbackProvider 注入:
runWithLoading包装异步任务emitHint展示提示showLoading/hideLoading控制全局 Loading
这些反馈不进入 BaseState,避免临时事件污染可渲染状态。BasePage / PageLogic 可通过 scope.presentation 或 presentation 访问;BaseVM / BaseAutoDisposeVM 不直接持有 Presentation 反馈服务。Loading 使用 token/counter 管理,并发 action 不会被静默跳过;runWithLoading 默认展示错误提示后继续抛出异常,需要吞掉异常时必须显式传入 rethrowError: false。
推荐页面开发流程:
Page 负责 UI 结构、Widget 组合、布局、样式
↓ 用户交互
PageLogic 负责页面本地 controller、临时交互状态、生命周期、调用 VM/Provider
↓ 调用动作
ViewModel / Notifier 负责可观察状态、业务动作编排、领域状态到 UI 状态的转换
↓ 调用抽象
Repository 负责业务数据获取
↓ 委托实现
DataSource / HttpClient 负责具体数据来源
应用统一从 Application.run() 启动,启动前初始化集中在 Application.bootstrap() 中,启动后的首跳由 AppHost 协调:
main()
→ Application.run(envConfig)
→ WidgetsFlutterBinding.ensureInitialized()
→ 初始化普通存储与安全存储
→ 恢复 AuthSession 到 authSessionProvider
→ appFeatureProviderOverrides 注入 Feature 默认 data 实现
→ createAppRouterOverrides() 注入 AppRouterConfig
→ AppExceptionCatcher.runAppGuarded()
→ ProviderScope(overrides: overrides)
→ MaterialApp.router
→ AppHost 在 Splash 后根据登录态跳转 RootRoute 或 LoginRoute
这样可以保证存储、登录会话、环境配置在应用挂载前完成初始化,同时把启动页停留与登录态首跳从业务 Feature 中解耦出来。
| 环境 | 入口 | 适用场景 |
|---|---|---|
| Dev | lib/main_dev.dart |
本地开发、调试、抓包 |
| SIT | lib/main_sit.dart |
测试环境、联调环境 |
| Prod | lib/main_prod.dart |
生产环境 |
EnvConfig 支持配置:
baseUrl、apiPathPrefix- 日志开关
- 代理/抓包配置
- 重试策略
- 证书校验策略
- 隐私协议、用户协议地址
网络层基于 Dio 封装,业务侧依赖统一的 HTTP 抽象,不直接散落 Dio 调用。
HttpConfig
↓
httpClientProvider
↓
BaseHttpClient / DioHttpClient
↓
Interceptor Chain + Cache + Retry + Mock + Batch Request
已内置能力:
- Token 自动注入
- 业务状态码解析
- 异常捕获与分类
- 日志输出
- 抓包代理支持
- 请求缓存策略
- 指数退避重试
- Mock 支持
- 批量请求封装
缓存策略包括:
noCache · cacheFirst · networkFirst · cacheOnly · networkOnly · staleWhileRevalidate
路由基于 GoRouter,但 Presentation/Page 层通过模板封装的路由定义与导航抽象使用,减少对第三方路由库的直接依赖。
Feature Route Node → AppPageRoute / AppShellRoute → GoRoute / StatefulShellRoute
Feature Module → AppFeature / AppTabEntry → AppFeatureRegistry
Feature DI → providerOverrides → domain binding Provider
App Composition → AppRouterConfig → goRouterProvider
Page Navigation → BaseNavigator → RouterNavigator
特点:
- 每个 Feature 自己维护路由定义
- 每个 Feature 通过
XxxFeature暴露元数据、模块路由、可选底部 Tab 入口与默认 Provider 装配 features/features.dart保留候选列表,并按当前环境创建唯一的AppFeatureRegistry- 注册表按
priority升序、Feature key 升序稳定排序,并校验 Feature key、route path 与 Tab key 唯一 features/exports.dart只导出业务页面需要的 route class 与公开类型,并由header.dart汇总lib/app/navigation/app_router_config.dart组合 Splash、Root/Shell、Feature 路由与 App 公共路由,并注入core/routerheader.dart只作为业务页面便捷入口,App/Core/Shared 内部使用精确 import,避免隐式依赖 Feature- App Shell 从当前环境注册表的 tabs 自动装配底部 Tab 分支;无 Tab 时不创建 Root redirect / Shell
- 支持公开路由与登录态路由
- 未登录访问受保护页面时自动跳转登录页
- 提供 root navigator key,支持非 UI 场景导航
模板内置通用登录会话模型与安全存储:
AuthSession
↓
AuthSessionController
↓
authSessionProvider
↓
Router Guard / AuthInterceptor / UI
AuthSession 以 token、refreshToken 与可扩展 payload 为核心,适配不同后端登录协议。authSessionProvider 是 App 内唯一响应式登录态状态源,负责守卫判断、HTTP token 注入与 UI 订阅;AuthSessionController 是语义化写入口,统一保存、清空和更新会话;AuthStore 只作为其安全存储后端。登录 Feature 中提供了完整示例:页面表单、ViewModel、Repository、DataSource、会话落盘与退出登录,其中 AuthRepositoryImpl 只返回 AuthSession,不直接写全局会话。
模板内置 webview_flutter 驱动的通用网页容器,位于 lib/shared/webview/,适合隐私政策、用户协议、帮助中心和业务 H5。它不作为业务 Feature 注册,公共路由由 App 路由组合层统一加入路由图。
快速打开公开网页:
ref.read(appRouterProvider).push(
const WebPageRoute(
url: 'https://example.com/privacy',
title: '隐私政策',
).location,
);需要登录的业务 H5 使用 AuthWebPageRoute:
ref.read(appRouterProvider).push(
const AuthWebPageRoute(url: 'https://example.com/member').location,
);需要 headers、白名单或 WebView 行为开关时,通过 extra 传入 WebPageConfig:
ref.read(appRouterProvider).push(
const AuthWebPageRoute().location,
extra: const WebPageConfig(
url: 'https://example.com/member',
allowedHosts: ['example.com'],
headers: {'X-Source': 'app'},
),
);未传 allowedHosts 时允许任意 http/https;传入后仅允许白名单 host。非 http/https 地址会被拦截,不会尝试外部 App 唤起。
主题系统支持亮/暗模式,并通过 Riverpod 持久化主题选择。
能力包括:
- Material 3 主题配置
ThemeExtension扩展语义化色值- 亮/暗主题资产切换
BuildContext扩展访问颜色与资源assets/images/与assets/images/dark/分离
示例:
context.appColor.brand
context.appAsset.logo国际化使用 Flutter 官方 gen-l10n:
- 源文件:
lib/core/l10n/arb/app_en.arb - 源文件:
lib/core/l10n/arb/app_zh.arb - 配置文件:
l10n.yaml - 生成脚本:
./script/gen_l10n.sh
切换语言示例:
ref.read(appLocaleProvider.notifier).setLocale(AppLocale.zh);模板将普通数据与敏感数据分开处理:
| 实现 | 底层 | 场景 |
|---|---|---|
PrefsStorage |
SharedPreferences | 主题、语言、普通偏好 |
SecureStorage |
FlutterSecureStorage | Token、登录态、敏感凭证 |
业务代码通过统一抽象访问存储,便于测试和替换实现。
shared/widgets 已内置常用组件:
| 组件 | 用途 |
|---|---|
| Toast / Loading | 全局轻提示、加载态 |
| Dialog / Sheet | 通用弹窗、确认面板、选择面板与业务选择面板 |
| RefreshView | 下拉刷新、上拉加载 |
| PrimaryRoundButton | 主按钮 |
| CountdownRoundButton | 验证码倒计时按钮 |
| InputTextWidget | 输入框 |
| ImageView | 本地、网络、SVG、Base64 图片展示 |
| SwitchWidget | 开关组件 |
| BaseCard | 基础卡片容器 |
| LabelRow | 标签行、箭头行、开关行 |
| ContextMenu / Overlay | 浮层与菜单能力 |
- 在
lib/features/下创建模块目录。 - 按
data / domain / presentation创建分层文件。 - 在
domain/repositories中定义 Repository 抽象。 - 在
domain/repositories中定义 Repository 抽象 Provider,在data/repositories中实现 Repository。 - 如页面存在可观察业务状态或动作编排,在
presentation/viewmodels中按状态保留需求继承BaseVM或BaseAutoDisposeVM管理 UI 状态与业务动作。 - 在
presentation/pages中继承BasePage编写 UI,并在page(scope)中读取状态、调用 ViewModel 或稳定 Provider。 - 在
<feature>_routes.dart中声明路由。 - 在
<feature>_feature.dart中继承AppFeature,声明唯一 key、priority、允许环境等元数据并暴露路由;如有默认 data 实现,在providerOverrides中装配 domain binding Provider。 - 在
features/features.dart中注册XxxFeature();App 路由配置与根ProviderScope会自动消费appFeatures。 - 如 route class 或公开类型需要给业务页面使用,在
features/exports.dart中导出。
ViewModel 只依赖 domain 抽象 Provider,不直接 import data/repositories 或 data/datasources。
AppFeatureMetadata.permissions 只声明业务权限 key,不执行鉴权;experimental 只标记实验模块。默认启用全部环境、无权限且非实验功能。注册表发现 Feature key、route path、Tab key 冲突或 Tab 引用了所属 Feature 未声明的路由时会立即失败。
推荐最小结构:
lib/features/order/
├── order_feature.dart
├── data/
│ ├── datasources/order_datasource.dart
│ └── repositories/order_repository_impl.dart
├── domain/
│ ├── entities/order.dart
│ ├── exceptions/order_exception.dart
│ └── repositories/order_repository.dart
└── presentation/
├── order_routes.dart
├── pages/order_page.dart
└── viewmodels/order_viewmodel.dart
如果只是简单页面,可以使用更薄的结构:
lib/features/profile/
├── profile_feature.dart
└── presentation/
├── pages/profile_page.dart
└── profile_routes.dart
- UI 逻辑放在 Page,业务动作放在 ViewModel。
- ViewModel 通过 Repository 抽象获取数据。
- 简单页面不要机械创建空 ViewModel、空 State 或空分层目录。
- 不在 Page 中直接调用 Dio、SharedPreferences、SecureStorage。
- 不让 Feature 直接依赖其他 Feature 的内部实现。
- 可复用 UI 放到
shared/widgets,可复用业务服务放到shared/services。
- 修改对应环境入口的
baseUrl与apiPathPrefix。 - 在 Feature 的 DataSource 中替换接口路径。
- 在 RepositoryImpl 中完成响应数据到业务实体的转换。
- 在 ViewModel 中调用 Repository 并更新状态。
本项目使用 Ruler 统一管理 Copilot、Cursor、Claude Code、Codex 等 AI Coding Assistant 指令。
- 指令源文件统一维护在
.ruler/。 - 不手动修改 Ruler 生成的
AGENTS.md、CLAUDE.md、.claude/skills/、.cursor/skills/、.codex/等文件。 - 修改
.ruler/后,先运行ruler apply --dry-run --verbose预览,再运行ruler apply。 - 提交时包含
.ruler/与.gitignore的变更,不提交生成文件。
详细说明见:Ruler AI 指令管理。
| 脚本 | 用途 |
|---|---|
./script/gen_l10n.sh |
生成国际化代码 |
./script/gen_app_icon.sh |
根据 assets/app_icon.png 生成 App 图标 |
./script/build_android.sh [apk|aab] |
构建 Android release 包,并收集混淆符号与 R8 mapping |
./script/build_ios.sh |
构建 iOS release IPA,并收集混淆符号与 dSYM |
dart run script/rename_project.dart <name> --package-id <id> --app-name <name> --en-app-name <name> |
重命名项目 |
# 生成国际化代码
./script/gen_l10n.sh
# 生成 App 图标
./script/gen_app_icon.sh
# 构建 Android APK,默认输出 apk
./script/build_android.sh
# 构建 Android AAB
./script/build_android.sh aab
# 构建 iOS IPA
./script/build_ios.sh
# 构建脚本会执行 flutter pub get,并使用 release + obfuscate + split-debug-info
# Android 产物归档到 app_release_packages/android/<version>/
# iOS 产物归档到 app_release_packages/ios/<version>/
# Dart 混淆符号表保存在对应归档目录的 symbols/ 子目录
# Android 会额外收集 mapping.txt 与 native-debug-symbols.zip(如存在)
# iOS 会额外收集 dSYMs,并在缺少 objective_c.framework.dSYM 时尝试用 dsymutil 补齐
# 重命名项目
dart run script/rename_project.dart my_app \
--package-id com.example.myapp \
--app-name "我的APP" \
--en-app-name "My App"
# 脚本会同步更新 Android、iOS、macOS、Linux、Web、Windows 的应用名、包名/Bundle ID 与启动配置
# 不会直接修改国际化生成物,也不会猜测公司名、版权、签名等发布主体信息
# 重命名后重新生成国际化代码
./script/gen_l10n.sh| 类型 | 依赖 |
|---|---|
| 状态管理 / DI | flutter_riverpod |
| 路由 | go_router |
| 网络 | dio、connectivity_plus |
| 存储 | shared_preferences、flutter_secure_storage |
| 国际化 | flutter_localizations、intl |
| UI / 体验 | bot_toast、easy_refresh、flutter_screenutil、cached_network_image、loading_animation_widget、shimmer、flutter_svg |
| 工程能力 | exception_catcher、logger、package_info_plus、permission_handler、flutter_launcher_icons |
MIT License · 可自由用于个人与商业项目