Skip to content

07 · 项目架构总览

目标:读完能在 449 个文件的目录树里10 秒内定位到任意功能,并理解一次请求的完整旅程。


开篇:前端概念对照

前端项目概念本项目对应
src/api/lib/data/services/api/
src/store/(Pinia/Vuex)lib/config/providers/ + lib/ui/*/view_models/
src/views/lib/ui/<feature>/views/
src/components/lib/he_components/ + lib/ui/<feature>/widgets/
src/router/lib/routing/
src/i18n/lib/core/i18n/ + assets/i18n/
src/utils/lib/utils/
main.jslib/main.dart
vite.config.jspubspec.yaml + scripts/build.sh
UI 组件库(antd/vant)lib/he_components/(自研)+ tdesign_flutter

一、目录地图

lib/449 个 Dart 文件。逐目录职责:

lib/
├── main.dart / main_prod.dart / main_test.dart     ← 三个入口(见第三节)

├── config/              41 个  依赖注入与环境配置
│   ├── providers/       34 个  Riverpod provider 注册中心 ⭐
│   ├── dependencies.dart       GoRouter provider + 启动 overrides
│   ├── post_privacy_bootstrap.dart  隐私同意后的 SDK 初始化(Phase 2)
│   ├── auth_bridge.dart       路由与鉴权的桥接
│   └── app_startup_observer.dart  启动性能观测

├── core/                38 个  跨模块基础设施(不含业务)
│   ├── i18n/                  国际化运行时引擎 ⭐
│   ├── iconfont/              远程 iconfont 字体加载
│   ├── permission/            系统权限守卫 ⭐
│   ├── platform/        13 个 平台能力(WebView / PDF / 权限实现)
│   ├── webview/               WebView 组件与 JS Bridge
│   ├── pdf/                   PDF 预览
│   └── app_constants.dart

├── data/               346 个  ⭐ 最大目录
│   ├── repositories/    48 个  Repository 三件套(abstract/remote/mock)
│   └── services/
│       ├── api/       291 个  Retrofit 接口、DTO、拦截器、SSE
│       └── local/             TokenStorage 等本地存储

├── domain/              41 个  跨层共享领域模型
│   └── models/                Result<T>、Role、PageDTO 等

├── generated/                 flutter_gen 生成物(assets.gen.dart)

├── he_components/       97 个  ⭐ 自研 UI 组件库
│   ├── src/widgets/     84 个  组件实现
│   ├── src/skeleton/     4 个  骨架屏
│   └── src/themes/       8 个  AppTokens / AppThemeData ⭐

├── i18n/  l10n/               国际化资源
├── routing/                   GoRouter 配置 ⭐
├── utils/               10 个  AppLogger 等工具

└── ui/                 348 个  ⭐ 业务页面,按 feature 组织
    ├── home/           33 个  首页(B 端安装商 / C 端用户)
    ├── device/         37 个  设备管理
    ├── device_detail/  23 个  设备详情
    ├── plant_detail/   39 个  电站详情
    ├── paramSet/       32 个  参数下发
    ├── auth/           34 个  登录 / 注册 / 找回密码
    ├── user/           20 个  用户中心
    ├── advancedFunction/ 19 个 高级功能
    ├── core/           14 个  跨 feature 的通用 UI(分页骨架等)⭐
    ├── devtools/       78 个  开发者工具(调试面板、组件展示)
    └── family/ plant_add/ plant_select/ privacy/ station_data_detail/ update/

定位口诀

我要找…去哪
某个页面长什么样lib/ui/<feature>/views/
页面的数据和逻辑lib/ui/<feature>/view_models/
页面的数据结构lib/ui/<feature>/models/
接口定义lib/data/services/api/
接口返回的数据模型lib/data/services/api/models/
数据来源(真实/Mock)lib/data/repositories/<feature>/
全局状态、DI 注册lib/config/providers/
一个通用组件lib/he_components/src/widgets/
颜色/间距/字体lib/he_components/src/themes/app_tokens.dart
路由路径lib/routing/routes.dart

二、分层职责

┌────────────────────────────────────────────────────────┐
│  View          lib/ui/<feature>/views/*.dart            │
│  ConsumerWidget / ConsumerStatefulWidget                │
│  职责:渲染 UI、捕获用户交互                              │
│  ≈ React 组件                                            │
└──────────────────┬─────────────────────────────────────┘
                   │ ref.watch(读状态)
                   │ ref.read(x.notifier).method()(触发动作)
┌──────────────────▼─────────────────────────────────────┐
│  ViewModel      lib/ui/<feature>/view_models/*.dart     │
│  @riverpod class XxxViewModel                           │
│  职责:业务逻辑、持有状态、调 Repository、DTO→UI Model     │
│  ≈ Vuex action + state                                   │
└──────────────────┬─────────────────────────────────────┘
                   │ ref.read(repositoryProvider)
┌──────────────────▼─────────────────────────────────────┐
│  Repository     lib/data/repositories/<feature>/        │
│  abstract + Remote + Mock 三件套                         │
│  职责:数据源抽象、错误包装为 Result<T>                    │
│  ≈ API service 层                                        │
└──────────────────┬─────────────────────────────────────┘
                   │ 调用
┌──────────────────▼─────────────────────────────────────┐
│  ApiService     lib/data/services/api/                  │
│  Retrofit @RestApi 抽象类                                │
│  职责:声明 HTTP 接口                                     │
│  ≈ Axios 封装                                            │
└──────────────────┬─────────────────────────────────────┘
                   │ Dio
┌──────────────────▼─────────────────────────────────────┐
│  拦截器链        auth → error → logging                  │
│  职责:注入 token、错误处理、日志                          │
│  ≈ Axios interceptors                                    │
└──────────────────┬─────────────────────────────────────┘
                   │ HTTP

                后端 API

职责边界表

层级做什么不做什么
View渲染、交互、临时 UI 状态(动画控制器、输入框控制器)业务逻辑、直接调 Repository、直接用 DTO
ViewModel业务逻辑、状态持有、DTO→UI Model 转换直接操作 UI、持有 BuildContext
Repository数据源选择、错误包装成 Result<T>UI 逻辑、持有状态
ApiService声明接口、序列化错误处理(交给 Repository 的 guard 函数)

⚠️ 最重要的两条红线

① UI 层禁止直接使用 DTO

DTO(lib/data/services/api/models/) → ViewModel 转换 → UI Model(lib/ui/<feature>/models/)

DTO 是后端契约,可能变化;UI Model 是页面需要的形态。ViewModel 负责转换

② View 不直接调 Repository

必须经 ViewModel。这样业务逻辑可测试、可复用。


三、启动流程:三阶段编排

lib/main.dart(440 行)是理解全局的关键。它把启动拆成三个阶段。

为什么要分阶段

隐私合规要求:用户同意隐私协议之前,不能初始化任何 SDK、不能发任何请求、不能读写凭证

所以启动被切成:

Phase 1  仅本地初始化(SharedPreferences 读主题/语言/环境)

隐私弹窗(未同意时)

Phase 2  同意后:SDK 初始化、凭证清理、路由挂载

全 App 运行

状态机

lib/main.dart:260-274

dart
@override
Widget build(BuildContext context) {
  final Widget app;
  if (!_themeLoaded || _lightTheme == null || _darkTheme == null) {
    app = _buildLoadingApp();        // 主题未就绪 → loading(启动图仍盖着)
  } else if (!(widget.privacyAlreadyAgreed || _agreedThisSession)) {
    app = _buildGateApp();           // 隐私未同意 → 门控 App(只弹窗)
  } else if (!_postPrivacyInitDone) {
    app = _buildLoadingApp();        // Phase 2 进行中 → loading
  } else {
    app = _buildFullApp();           // 完成 → 全 App(MaterialApp.router)
  }
  return app;
}

四个状态,三棵 App 树——注意 _buildGateApp()独立的 MaterialApp,不挂路由、不构造 AuthViewModel

Phase 1:main()appMain()

lib/main.dart:74-115

dart
Future<void> appMain({Environment? forcedEnvironment}) async {
  final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);   // 保留启动图
  Future.delayed(const Duration(seconds: 5), FlutterNativeSplash.remove);  // 5s 兜底

  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);  // 锁竖屏

  AppLogger.I.init();

  NetworkInspectorConfig.apply();      // chucker 抓包(debug/profile)
  TDTheme.needMultiTheme();            // TDesign 多主题支持
  _configureEasyLoading(AppThemeData.light);

  final overrides = await bootstrapOverrides(forcedEnvironment: forcedEnvironment);

  final prefs = await SharedPreferences.getInstance();
  final privacyAlreadyAgreed = prefs.getBool(PrivacyKeys.agreed) ?? false;

  runApp(ProviderScope(
    overrides: overrides,
    observers: [AppStartupObserver()],
    child: AppBootstrap(...),
  ));
}

三个入口文件

文件作用命令
lib/main.dart默认入口,环境走运行时解析fvm flutter run
lib/main_test.dart强制锁定测试环境后端fvm flutter run -t lib/main_test.dart
lib/main_prod.dart强制锁定生产环境后端fvm flutter run -t lib/main_prod.dart

⚠️ main_test.darttest 指的是「后端测试环境」,不是单元测试。这是最容易误解的命名。

dart
// lib/main_test.dart 全文
void main() => appMain(forcedEnvironment: Environment.test);

Phase 2:runPostPrivacyInit

用户点击「同意」后触发(lib/main.dart:247-257):

dart
Future<void> _runPostPrivacyInit() async {
  await runPostPrivacyInit(isExistingInstall: widget.isExistingInstall);
  unawaited(
    ref.read(i18nSyncServiceProvider)
        .sync(resolveEffectiveLocale(ref.read(localeNotifierProvider))),
  );
  if (mounted) setState(() => _postPrivacyInitDone = true);
}

注意 mounted 检查(呼应 03 章)。

全 App 树的关键装配

lib/main.dart:334-399_buildFullApp() 装配顺序(从外到内):

MaterialApp.router
  └─ builder: MediaQuery(textScaler: noScaling)      ← 固定字体缩放 1.0
      └─ EasyLoading.init()                          ← 全局 toast/loading overlay
          └─ ScreenUtilInit(designSize: 750×1624)    ← 等比缩放
              └─ Stack
                  ├─ child(路由页面)
                  ├─ NetworkOfflineBanner            ← 离线横幅
                  ├─ _IconfontBootstrap              ← 后台加载远程字体
                  └─ DevFloatingButton(!kReleaseMode)← 开发调试悬浮按钮

!kReleaseMode 的用法值得记住:debug 和 profile 都会装载开发工具,只有 release 不装。

依赖注入:bootstrapOverrides

lib/config/dependencies.dart:39-52

dart
Future<List<Override>> bootstrapOverrides({Environment? forcedEnvironment}) async {
  await ThemeModeNotifier.init();
  await UseMockApi.init();
  await LocaleNotifier.init();

  final appEnv = forcedEnvironment ?? await EnvironmentConfig.getEnvironment();
  RuntimeEnvironment.current = appEnv;

  return [];
}

为什么要预加载:让 ThemeModeNotifier / UseMockApi / LocaleNotifier同步读取 SharedPreferences 值,避免首帧闪烁。

自检

  • [ ] 能说出启动的三个阶段及其存在原因(隐私合规)
  • [ ] 知道 main_test.dart 是「测试环境后端」入口,不是单元测试
  • [ ] 知道 _buildGateApp() 是不挂路由的独立 App
  • [ ] 知道 !kReleaseMode 用于装载开发工具

四、一次请求的完整旅程

以「首页加载电站列表」为例:

mermaid
flowchart TD
    V["View<br/>AgentHomePage<br/>ref.watch(agentHomeViewModelProvider)"] -->|1 ref.watch| VM["ViewModel<br/>AgentHomeViewModel<br/>@riverpod class"]
    V -->|"2 ref.read(vm.notifier).refresh()"| VM
    VM -->|3 ref.read| R["Repository 抽象<br/>PlantRepository"]
    R -->|4 DI 切换| RR["PlantRepositoryRemote<br/>guardApiObject()"]
    R -->|4' Mock 模式| RM["PlantRepositoryMock<br/>读 assets/mock_data/*.json"]
    RR -->|5| A["ApiService<br/>PlantApiService<br/>Retrofit @RestApi"]
    A -->|6| I["Dio 拦截器链<br/>auth → error → logging"]
    I -->|7 HTTP| S["后端 API"]
    S -->|8 响应| I
    I -->|9| RR
    RR -->|"10 Result&lt;T&gt;"| VM
    VM -->|"11 AsyncValue 三态"| V

逐步拆解

做什么关键代码
1View 订阅状态ref.watch(agentHomeViewModelProvider)
2用户下拉刷新ref.read(vmProvider.notifier).refresh()
3ViewModel 拿 Repositoryref.read(plantRepositoryProvider)
4DI 决定用 Remote 还是 Mocklib/config/providers/
5Repository 调 ApiServiceguardApiObject(() => _api.getXxx())
6Retrofit 发请求生成的 .g.dart 实现
7拦截器注入 token、记录日志auth / error / logging
8-9响应沿拦截器链返回
10错误被包装成 Result<T>Ok(value) / ErrorResult(error)
11ViewModel 转成 AsyncValue 三态View 自动重建

第 10 步是关键设计:Repository 不抛异常,而是把错误包进 Result<T>。ViewModel 用模式匹配处理。详见 09-数据层链路


五、跨模块基础设施

lib/core/ 下的能力,业务代码按需取用:

i18n 国际化

lib/core/i18n/
├── runtime_i18n.dart            RuntimeI18n 访问器(`ofNonNull(context)`)
├── runtime_i18n_delegate.dart   LocalizationsDelegate 实现
├── runtime_i18n_engine.dart     查表/插值/复数引擎(零 Flutter 依赖)
├── i18n_cache.dart              远程词条磁盘缓存
├── i18n_remote_source.dart      Dio 拉取后端词条
├── i18n_sync_service.dart       同步编排
├── app_languages.dart           支持的语言列表
└── i18n_utils.dart

词条资源在 assets/i18n/<tag>.json。详见 14-国际化

iconfont 图标

lib/core/iconfont/ 远程加载 iconfont 字体,使用方式:

dart
AppIcon(name: 'icon_search', size: 20, color: tokens.textPrimary)

不用 Icon(Icons.search)

permission 权限

lib/core/permission/
├── permission_guard.dart     权限请求守卫(说明弹窗 → 请求 → 引导设置)
├── permission_status.dart    权限状态枚举 + extension
├── permission_rationale.dart 权限说明文案模型
└── permission_service.dart   ⚠️ 废弃中(见下)

⚠️ permission_service.dart 是废弃链路。它只出现在 5 个文件(定义 + DI 注册),业务页面零使用。详见 15-权限与设备能力

platform / webview / pdf

  • lib/core/platform/(13 个):平台能力抽象与实现(权限、设备信息)
  • lib/core/webview/:WebView 组件 + JS Bridge(H5 与原生通信)
  • lib/core/pdf/:PDF 预览页

六、主教材范例:agent_home_page.dart

这是全项目最值得精读的一个文件——它的文件头注释本身就是一份规范说明。

lib/ui/home/views/agent_home_page.dart:1-15

dart
// HeListScaffold(标题 + 搜索「请输入电站」+ 登出 action)+ subheader(排序/筛选
// 按钮行,固定)+ PagedListBody<PlantDto>(buildListHeader: banner 轮播
// HomeBanner + 统计卡片行 HomeStatsBar,随列表滚动;对齐 uni-app
// scroll-list 内 banner->统计->列表 同滚动结构)。
// ...
// 守 coding-standards:HeListScaffold + PagedListBody(禁手写 EasyRefresh 装配)、
// AppTokens、he_components barrel、RuntimeI18n、HeLoading、GoRouter。

导入区的规范(agent_home_page.dart:17-42

dart
import 'package:flutter/material.dart';
import 'package:haierenergy/core/i18n/runtime_i18n.dart';        // ① 多语言
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import 'package:haierenergy/generated/assets.gen.dart';           // ② 图片强类型
import 'package:haierenergy/he_components/he_components.dart';    // ③ 组件库 barrel

import '../../../config/dependencies.dart';
import '../../../data/services/api/api_response_helpers.dart';
import '../../../data/services/api/models/plant_dto.dart';        // ④ DTO
import '../../../domain/models/result.dart';
import '../../core/utils/api_error_message.dart';
import '../../../routing/routes.dart';
import '../../core/models/paged_state.dart';                      // ⑤ 分页状态
import '../../core/widgets/paged_list_body.dart';                 // ⑥ 分页 body
import '../../auth/models/auth_state.dart';
import '../../auth/view_models/auth_view_model.dart';
import '../view_models/agent_home_view_model.dart';
import '../widgets/home_banner.dart';

六种 import 缺一不可,这就是「一个新页面应该长什么样」的答案。

权限控制渲染(:57-62

dart
final l10n = RuntimeI18n.ofNonNull(context);
final tokens = AppTokens.of(context);
final canAddPlant =
    ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');
final canDeletePlant =
    ref.watch(authViewModelProvider).hasPermission('app:b:deletePlant');

模式:先算出权限布尔值,再传给子组件决定渲染。详见 15-权限与设备能力

状态监听做副作用(:69-75

dart
ref.listen<int>(homeShowTickProvider, (previous, next) {
  if (ref.read(detailClickProvider)) {
    ref.read(detailClickProvider.notifier).state = false;
  } else {
    _onPlantDataChanged();
  }
});

ref.listen 用于「状态变化触发一次性动作」(弹 toast、刷新、导航),不用于渲染


七、自检清单

  • [ ] 能说出 lib/ 下 7 个一级目录各自职责和文件量级
  • [ ] 面对「找某个功能」能在 10 秒内定位目录
  • [ ] 能画出一次请求从 View 到后端再返回的链路
  • [ ] 知道为什么启动要分三阶段
  • [ ] 知道 main_test.dart 的真实含义
  • [ ] 知道 UI 层禁止直接用 DTO、禁止直接调 Repository
  • [ ] 能说出 agent_home_page.dart 导入区的六类 import

下一步

08 · 状态管理 Riverpod