IM App 整体架构设计
小型 IM 应用完整架构方案
Clean Architecture / MVVM / Feature 驱动 / 高内聚低耦合 / 严格分层
Part 0:开发环境配置
阅读架构之前先把环境跑起来,大约 5 分钟。
新机器初始化(只需做一次)
第零步:从 Gitea 拉取代码
项目托管在内部 Gitea,仅支持 HTTPS 访问(SSH 暂不开放)。需要先在 Gitea 生成个人访问令牌(Personal Access Token)。
生成 Token 步骤:
- 登录 Gitea(
https://gitea.winwayinfo.com) - 点击右上角头像 → Settings
- 左侧菜单选 Applications
- 在 "Generate New Token" 填写令牌名称(任意)
- Repository and organization Access 下选择 All
- Selected token permissions 下所有权限选 Read and Write
- 点击 Generate Token,复制生成的 token(只显示一次)
克隆仓库:
# 将 YOUR_TOKEN 替换为刚才生成的 token,YOUR_USERNAME 替换为你的 Gitea 用户名
git clone https://YOUR_USERNAME:YOUR_TOKEN@gitea.winwayinfo.com/CUS-IM/customer-im-client.git
# 或者先 clone 再配置凭据
git clone https://gitea.winwayinfo.com/CUS-IM/customer-im-client.git
# 输入用户名和 token(token 作为密码)
- Token 只在生成时显示一次,务必立即保存
- 如果 clone 时提示输入密码,输入的是 Token 而不是账号密码
- 为避免每次 push/pull 都输入,可在克隆地址里内嵌 token:
https://user:token@gitea.winwayinfo.com/... - 切换到
dev分支开发:git checkout dev;main为主干保护分支
第一步:安装 Flutter SDK
- 前往 flutter.dev 下载最新 stable channel macOS 版本(.tar.xz)
Apple Silicon(M 系列)必须下载 arm64 版本。
文件名含arm64的才是 Apple Silicon 版,例如flutter_macos_arm64_3.x.x-stable.tar.xz。
不含arm64的(如flutter_macos_3.x.x-stable.tar.xz)是 Intel (x86_64) 版,装在 M 芯片上编译 macOS 时会报:
Unable to find a device matching { platform:macOS, arch:arm64 } - 解压到固定目录,推荐
~/flutter:cd ~ tar xf ~/Downloads/flutter_macos_arm64_*.tar.xz # 解压后目录为 ~/flutter - 写入环境变量:
echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.zshrc - 验证(在当前终端临时生效):
source ~/.zshrc flutter --version dart --version # 确认 Dart 二进制是 arm64(Apple Silicon 机器) file $(which dart) # 应输出 Mach-O 64-bit executable arm64
第二步:安装 Homebrew + Ruby + CocoaPods(iOS / macOS 必须)
iOS 和 macOS 平台依赖 CocoaPods 管理原生依赖,CocoaPods 需要 Ruby 3.x,通过 Homebrew 安装。
Ruby 4.0 + OpenSSL 3.6 在 macOS 26 beta 上与
cdn.cocoapods.org 的 TLS 握手不兼容,pod install 会报
Connection reset by peer - SSL_connect,导致 iOS / macOS 构建失败。
# 1. 安装 Homebrew(需要输入开机密码)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. 将 Homebrew 本身写入 PATH(Apple Silicon M 芯片路径)
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# 3. 验证 brew 可用
brew --version
# 4. 安装 Ruby 3.3(固定版本,不能用 brew install ruby 因为会装 4.x)
brew install ruby@3.3
# 5. 将 Ruby 3.3 写入 PATH(必须在系统 Ruby 2.6 之前)
echo 'export PATH="/opt/homebrew/opt/ruby@3.3/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# 6. 验证 Ruby 版本(确认是 3.3.x)
ruby --version
# 7. 安装 CocoaPods
gem install cocoapods
# 8. pod 可执行文件路径写入 PATH
# gem 路径固定为 3.3.0
echo 'export PATH="/opt/homebrew/lib/ruby/gems/3.3.0/bin:$PATH"' >> ~/.zshrc
# 9. 终端编码设置(避免 pod 报 UTF-8 警告)
echo 'export LANG=en_US.UTF-8' >> ~/.zshrc
source ~/.zshrc
# 11. 验证
pod --version
第 10 步单独说明——在 ~/.zshrc 末尾手动追加以下内容(这段是写入文件的 shell 代码,不是在终端直接执行的命令):
# CocoaPods CDN SSL fix for macOS 26 beta
# 修复原因:macOS 26 beta + OpenSSL 3.x 与 cdn.cocoapods.org TLS 握手失败
# 每次开终端自动写入临时配置文件并设置 OPENSSL_CONF,pod install 时读取该配置
cat > /tmp/openssl_pod.cnf << 'OPENSSL_EOF'
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT:@SECLEVEL=1
OPENSSL_EOF
export OPENSSL_CONF=/tmp/openssl_pod.cnf
- 必须用
brew install ruby@3.3,不能用brew install ruby(后者会装最新的 4.x) - Homebrew 装好后必须先把
/opt/homebrew/bin写入 PATH,否则终端找不到brew命令 - macOS 自带 Ruby 2.6,CocoaPods 要求 >= 3.0,必须通过 Homebrew 安装新版
- OPENSSL_CONF 修复是 写在 ~/.zshrc 里的 shell 代码段,每次开新终端自动执行,不是一次性命令
- iOS / macOS 编译还需要完整安装 Xcode,仅有 Command Line Tools 不够(见下一步)
(补充)安装 Xcode(iOS / macOS 编译必须)
CocoaPods 只是依赖管理工具,实际编译 iOS / macOS 需要完整的 Xcode。Command Line Tools 不够。
- 在 App Store 搜索 Xcode 并安装(约 10 GB)
- 安装完成后执行:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcodebuild -runFirstLaunch xcodebuild -version # 验证
flutter build ios 报 "Application not configured for iOS",flutter build macos 报 "unable to find utility xcodebuild",两个平台都无法编译。
第三步:配置 IDE
Android Studio
- 打开 Android Studio
- 菜单:
Settings(Windows/Linux)或Preferences(macOS) - 导航至
Languages & Frameworks → Flutter - Flutter SDK path 填写解压路径,例如
/Users/{你的用户名}/flutter - 点击 Apply → OK
- 重启 Android Studio
VS Code
- 在 Extensions 市场搜索并安装 Flutter 插件(Dart 插件会自动一并安装)
- 按
Cmd+Shift+P打开命令面板 - 输入
Flutter: Change SDK,选择 Flutter SDK 解压目录(例如/Users/{你的用户名}/flutter) - 重启 VS Code
第三步:运行初始化脚本
重新打开终端,进入项目根目录执行:
cd customer-im-client
bash scripts/setup.sh
setup.sh 包含 6 步:安装全局工具 → 配置 PATH → dart pub get → melos bootstrap → mason get → melos run gen。
其中 melos bootstrap 负责生成 .iml 和 .idea/modules.xml(IDE 模块识别,不入库)。
若提示
melos: command not found,说明 PATH 未生效,执行 source ~/.zshrc 后再重试。
若 IDE 缺少
[melos_xxx_sdk] 标签,单独执行 melos bootstrap 即可补全。
第四步:验证项目可以运行
重新打开 IDE,在内嵌终端或新终端窗口中,切换到 im_app 目录执行:
cd customer-im-client/apps/im_app
flutter run
首次运行会触发 Gradle / CocoaPods 等平台工具链下载,耐心等待即可。
第五步:各平台单独跑一次 pub get
为避免平台插件注册缺失导致运行失败,每个目标平台在首次开发前单独执行一次:
# 在 apps/im_app 目录下执行
flutter pub get # 所有平台依赖
# 按需对各平台单独验证
flutter build apk --debug # Android
flutter build ios --debug --no-codesign # iOS(macOS 机器)
flutter build macos --debug # macOS
完成以上步骤后,~/.zshrc 末尾应包含以下内容(供核对):
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/opt/homebrew/opt/ruby@3.3/bin:$PATH"
export PATH="/opt/homebrew/lib/ruby/gems/3.3.0/bin:$PATH"
export PATH="$HOME/flutter/bin:$PATH"
export LANG=en_US.UTF-8
# CocoaPods CDN SSL fix for macOS 26 beta
cat > /tmp/openssl_pod.cnf << 'OPENSSL_EOF'
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT:@SECLEVEL=1
OPENSSL_EOF
export OPENSSL_CONF=/tmp/openssl_pod.cnf
melos run gen:watch 常驻运行即可(见下方日常工作流)。
日常开发工作流
每次开发时开两个终端窗口:
| 终端 | 命令 | 说明 |
|---|---|---|
| Terminal 1(常驻) | melos run gen:watch | ⚠️ 必须保持运行,保存代码后自动生成 .g.dart |
| Terminal 2 | flutter run | 启动 App,支持热重载 |
代码生成常见问题
- 保存后红线不消失:检查 Terminal 1 是否有
melos run gen:watch在运行 - 生成报错:
melos run gen重新全量生成 - 新增依赖后:先
dart pub get,再重启 watch - .g.dart 冲突:直接删除冲突文件后重新生成,不要手动合并
Melos 命令速查
| 命令 | 说明 |
|---|---|
| 依赖 & 代码生成 | |
bash scripts/setup.sh | 新机器一键环境初始化(只需执行一次) |
melos bootstrap | 生成 IDE 模块配置(.iml + modules.xml),缺 [melos_xxx] 标签时执行 |
dart pub get | 安装所有依赖(首次或 pubspec 变更后) |
melos run gen | 一次性代码生成(.g.dart / .freezed.dart) |
melos run gen:watch | ⚠️ 开发必开:监听模式,保存后自动生成 |
| 质量检查 | |
melos run analyze | 所有 Package 静态分析 |
melos run test | 所有 Package 单元测试 |
| 清理 | |
melos run clean | 所有 Package flutter clean |
melos run clean:deep | 深度清理(含 Gradle / Pods / CMake + 生成文件) |
melos run clean:deep -- android | 只清 Android 平台缓存(ios / macos / windows 同理) |
| SDK 版本管理 | |
melos run sdk:bump | 从 flutter.dev 拉最新稳定版,Dart + Flutter 一起升级 |
melos run sdk:bump -- --dart 3.12.0 | 只升 Dart SDK 约束,Flutter 下限不变 |
melos run sdk:bump -- --flutter 3.40.0 | 只升 Flutter 下限,Dart 不变 |
melos run sdk:bump -- --dart 3.12.0 --flutter 3.40.0 | 手动指定两者(用于 CI 固定版本) |
| 构建 | |
melos run new:sdk -- push | 创建新 SDK 包 packages/push_sdk/(Facade+Wiring 骨架) |
melos run remove:sdk -- push | 删除 SDK 包,同步清理 workspace、IDE 模块注册 |
melos run build:android:apk | 构建 Android APK |
melos run build:android:aab | 构建 Android AAB(Google Play) |
melos run build:ios | 构建 iOS IPA(仅 macOS) |
melos run build:macos | 构建 macOS app |
melos run build:windows | 构建 Windows EXE(仅 Windows) |
第一部分:为什么(Why)- 设计理念
Mono-Repo 架构
项目组织方式:本项目采用 Mono-Repo(单一代码仓库)方式组织,使用 Melos 进行管理。
什么是 Mono-Repo?
Mono-Repo 是将多个相关项目放在同一个代码仓库中管理的方式,与传统的每个项目一个仓库(Multi-Repo)不同。
我们的 Mono-Repo 包含:
- 主应用(IM App)
- 9 个 Core SDK(NetworkSDK、StorageSDK、ProtocolSDK、MediaSDK、RTCSDK、NotificationSDK、CipherGuardSDK、CryptoSDK、L10nSDK)
- 共享组件库(Widgets、Utils、Extensions)
- 示例应用和测试项目
为什么选择 Mono-Repo?
| 优势 | 说明 | 实际价值 |
|---|---|---|
| 版本一致性 | 同一个 commit 保证所有 package 兼容 | 不会出现版本不匹配问题 |
| API 变更安全 | 编译期立即发现问题,马上修复 | 不会在发版后才发现问题 |
| 重构成本低 | 一次性全 repo 重构 | 不需要跨 repo、分批跟进 |
| 开发效率高 | 改 SDK → 主应用立即验证 | 不需要先发版才能验证 |
| 统一工具链 | 一套 Melos 指令管理所有项目 | 不需要维护多套脚本 |
| 新人友好 | clone 一个 repo 就全到位 | 不需要 clone 多个 repo |
Melos 管理工具
Melos 是 Flutter/Dart 生态的 Mono-Repo 管理工具,提供:
- 自动依赖解析:自动 link 本地 package,无需手动管理
- 统一脚本命令:一条命令运行所有测试、构建、发布
- 增量测试:只测试受影响的 packages,节省时间
- 版本管理:统一管理所有 package 的版本号
IM App] App --> Root[IM Mono-Repo
根目录] Root --> SDKs[Core SDKs
9个独立SDK] Root --> Shared[共享组件
Widgets/Utils] Root --> Examples[示例应用
Example Apps] SDKs --> SDK1[NetworkSDK] SDKs --> SDK2[StorageSDK] SDKs --> SDK3[ProtocolSDK] SDKs --> SDK4[MediaSDK] SDKs --> SDK5[RTCSDK] SDKs --> SDK6[NotificationSDK] SDKs --> SDK7[CipherGuardSDK
Flutter Plugin] SDKs --> SDK8[CryptoSDK
占位] SDKs --> SDK9[L10nSDK] App -.依赖.-> SDKs App -.依赖.-> Shared Examples -.依赖.-> SDKs style App fill:#e3f2fd,stroke:#0288d1,stroke-width:3px style Root fill:#fff9c4,stroke:#f57f17,stroke-width:2px style SDKs fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Shared fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Examples fill:#fff4e6,stroke:#f57c00,stroke-width:2px
设计理念:通过 Mono-Repo + Melos 的组合,我们能够在保持模块独立性的同时,获得统一管理的便利性,大幅提升开发效率和代码质量。
Packages 目录结构(SDK 独立 Package)
所有可复用的基础能力 SDK 从主 App 的 core/foundation/ 提取为独立 Package,放在 Mono-Repo 的 packages/ 目录下。主 App 通过 pubspec.yaml 的 path: 依赖引用。
packages/
│
├── networks_sdk/ # 网络通信(HTTP + WebSocket,Flutter Plugin)
│ ├── build.yaml # @ApiRequest 代码生成器注册
│ └── lib/
│ ├── networks_sdk.dart # barrel file(统一导出)
│ └── src/
│ ├── annotations/
│ │ └── api_request.dart # @ApiRequest 注解定义
│ ├── generator/
│ │ ├── api_request_generator.dart # build_runner 代码生成器实现
│ │ └── builder.dart # SharedPartBuilder 入口
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── http/
│ │ │ │ ├── api_client.dart # Dio REST 客户端
│ │ │ │ ├── token_refresh_manager.dart # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用)
│ │ │ │ └── interceptor/
│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
│ │ │ │ ├── encryption_interceptor.dart # 加密拦截器(预留给 cipher_guard_sdk)
│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试 + 业务错误钩子
│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
│ │ │ ├── socket/
│ │ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream/加密钩子)
│ │ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口
│ │ ├── dto/
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析
│ │ └── repositories/
│ │ ├── networks_sdk_repository_impl.dart
│ │ └── networks_messaging_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── api_error.dart # @freezed HTTP 错误联合类型(7 变体)
│ │ │ ├── encrypted_request.dart # 加密请求结果数据类
│ │ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
│ │ │ ├── socket_connection_state.dart # 连接状态 enum
│ │ │ ├── http_method.dart # GET/POST/PUT/DELETE/PATCH
│ │ │ └── api_request_type.dart # request/login/upload/stream/download
│ │ └── repositories/
│ │ ├── networks_sdk_repository.dart
│ │ └── networks_messaging_repository.dart
│ └── presentation/
│ ├── facade/
│ │ ├── networks_sdk_api.dart # HTTP 公开 API 接口
│ │ └── networks_messaging_api.dart # WebSocket 公开 API 接口
│ └── wiring/
│ ├── api_config.dart # HTTP 配置(baseURL/Token/回调/加密/重试)
│ ├── socket_config.dart # WebSocket 配置(心跳/重连/加密钩子)
│ ├── network_callbacks.dart # 回调类型定义(认证/加密/业务错误/下载/WS)
│ ├── networks_sdk_core.dart
│ ├── networks_sdk_api_impl.dart
│ ├── networks_messaging_api_impl.dart
│ └── networks_sdk_wiring.dart # 工厂:build() / buildMessagingApi()
│
├── cipher_guard_sdk/ # 端对端加密(Flutter Plugin)
│ └── lib/
│ ├── cipher_guard_sdk.dart # barrel file
│ └── src/
│ ├── data/
│ │ ├── constants/
│ │ │ └── method_channel_constants.dart
│ │ ├── datasources/
│ │ │ ├── encryption_flutter_service.dart # RSA/AES 纯 Dart 加解密(pointycastle + encrypt)
│ │ │ └── encryption_method_channel.dart # Native 密钥同步(iOS App Group)
│ │ ├── dto/
│ │ │ ├── chat_encryption_key_dto.dart
│ │ │ ├── encrypted_message_dto.dart
│ │ │ ├── method_channel_response.dart
│ │ │ └── rsa_key_pair_dto.dart
│ │ └── repositories/
│ │ └── encryption_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── chat_encryption_key.dart
│ │ │ ├── encrypted_message.dart
│ │ │ ├── rsa_key_pair.dart
│ │ │ └── session_key.dart
│ │ └── repositories/
│ │ └── encryption_repository.dart
│ └── presentation/
│ ├── facade/
│ │ └── cipher_guard_sdk_api.dart # 公开 API(调用方只依赖这里)
│ └── wiring/
│ ├── cipher_guard_sdk_api_impl.dart
│ ├── cipher_guard_sdk_core.dart
│ └── cipher_guard_sdk_wiring.dart
│
├── storage_sdk/ # 本地存储(Facade+Wiring,纯基础设施:连接管理 + 通用 CRUD)
│ └── lib/
│ ├── storage_sdk.dart # barrel:导出 StorageSdkApi
│ └── src/
│ ├── data/
│ │ ├── local/
│ │ │ └── datasources/
│ │ │ └── database_datasource.dart # 连接生命周期(openDatabase/closeDatabase)
│ │ └── repositories/
│ │ └── database_repository_impl.dart # 通用 CRUD 的 Drift 实现
│ ├── domain/
│ │ └── repositories/
│ │ └── database_repository.dart # 通用 CRUD 接口(泛型,与表无关)
│ └── presentation/
│ ├── facade/
│ │ └── storage_sdk_api.dart # 公开接口(生命周期 + 全量 CRUD)
│ └── wiring/
│ ├── storage_sdk_api_impl.dart # 委托给 Core
│ ├── storage_sdk_core.dart # 持有 DataSource + Repo
│ └── storage_sdk_wiring.dart # build(databaseFactory:) → StorageSdkApi
│
│ # ── 以下 SDK 均遵循相同的 Facade + Wiring 模式,结构参照 cipher_guard_sdk ──
│
├── notification_sdk/ # 推送通知(Flutter Plugin)
│ └── lib/notification_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── media_sdk/ # 媒体处理(Flutter Plugin)
│ └── lib/media_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── rtc_sdk/ # 实时音视频(Flutter Plugin)
│ └── lib/rtc_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── protocol_sdk/ # 消息协议(Flutter Plugin)
│ └── lib/protocol_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── l10n_sdk/ # 国际化(Flutter Plugin)
│ └── lib/l10n_sdk.dart + src/{data,domain,presentation}/ (同上模式)
Package 类型说明:
- Flutter Plugin(多个):所有 SDK 均声明为 Flutter Plugin,包含
android/+ios/原生代码入口,Plugin 机制自动注册 - cipher_guard_sdk:E2E 加密核心,RSA/AES 双层加密 + iOS App Group 密钥同步(用于推送通知解密)
主 App 引用方式(pubspec.yaml):
dependencies:
networks_sdk:
path: packages/networks_sdk
cipher_guard_sdk:
path: packages/cipher_guard_sdk
storage_sdk:
path: packages/storage_sdk
notification_sdk:
path: packages/notification_sdk
media_sdk:
path: packages/media_sdk
rtc_sdk:
path: packages/rtc_sdk
protocol_sdk:
path: packages/protocol_sdk
l10n_sdk:
path: packages/l10n_sdk
设计原则:
- 独立发布:每个 SDK 可独立版本号、独立 CHANGELOG,未来可发布到 pub.dev
- 跨项目复用:其他 Flutter 项目可直接依赖这些 SDK,不绑定 IM App 业务逻辑
- 最小依赖:SDK 之间尽量无依赖,必要时通过接口解耦
- Melos 统一管理:
dart pub get统一安装依赖,melos run test批量测试
代码生成工具
本项目大量使用代码生成工具来提升开发效率、减少样板代码,并保证类型安全。
核心代码生成工具
| 工具 | 作用 | 生成内容 | 优势 |
|---|---|---|---|
| riverpod_generator | Riverpod Provider 代码生成 | 自动生成 Provider、依赖注入代码 | 大幅减少样板代码,编译期类型安全 |
| freezed | 不可变数据类生成 | State 类、copyWith、序列化代码 | 消除手写 State 的繁琐,保证不可变性 |
| json_serializable | JSON 序列化/反序列化 | fromJson/toJson 方法 | 自动处理 JSON 转换,类型安全 |
| build_runner | 代码生成执行器 | 监听文件变化,自动生成 | 开发时实时生成,无需手动执行 |
代码生成示例对比
不使用代码生成(手写样板代码)
// 手写 State 类 - 繁琐且容易出错
class ChatState {
final List<Message> messages;
final bool isLoading;
final String error;
const ChatState({
required this.messages,
required this.isLoading,
required this.error,
});
// 手写 copyWith - 每个字段都要写
ChatState copyWith({
List<Message>? messages,
bool? isLoading,
String? error,
}) {
return ChatState(
messages: messages ?? this.messages,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
// 手写 equality - 容易遗漏字段
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatState &&
listEquals(other.messages, messages) &&
other.isLoading == isLoading &&
other.error == error;
}
@override
int get hashCode => Object.hash(messages, isLoading, error);
}
// 手写 Provider - 样板代码多
final chatViewModelProvider = StateNotifierProvider.autoDispose<
ChatViewModel, ChatState
>((ref) {
final sendMessageUseCase = ref.watch(sendMessageUseCaseProvider);
final loadMessagesUseCase = ref.watch(loadMessagesUseCaseProvider);
return ChatViewModel(sendMessageUseCase, loadMessagesUseCase);
});
使用代码生成(简洁高效)
// 使用 @freezed - 自动生成 copyWith、equality、toString
part 'chat_state.freezed.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List<Message> messages,
@Default(false) bool isLoading,
@Default('') String error,
}) = _ChatState;
}
// 使用 @riverpod - 自动生成 Provider
part 'chat_view_model.g.dart';
@riverpod
class ChatViewModel extends _$ChatViewModel {
@override
ChatState build() => const ChatState();
Future<void> sendMessage(String content) async {
state = state.copyWith(isLoading: true);
await ref.read(sendMessageUseCaseProvider)(content);
state = state.copyWith(isLoading: false);
}
}
开发流程
代码生成命令
# 一次性生成
melos run gen
# 监听模式(开发期间必须常驻)
melos run gen:watch
代码生成的价值
- 大幅减少样板代码:不需要手写 copyWith、equality、hashCode
- 编译期类型安全:生成的代码类型完全正确,无运行时错误
- 自动化维护:修改字段后自动重新生成,无需手动同步
- 统一代码风格:生成的代码风格一致,易于 Code Review
- 提升开发效率:专注业务逻辑,不浪费时间在重复代码上
设计理念与目标
Clean Architecture(整洁架构)
目的:让业务逻辑与界面设计分离
方法:通过结构的分层来约束类别间的使用方向
好处:
- 代码更易维护
- 代码更易测试
- 代码更易扩展
- 业务逻辑独立于框架
- 业务逻辑独立于UI
- 业务逻辑独立于数据库
MVVM(Model-View-ViewModel)状态管理
演进历史
在响应式应用中,状态管理经历了以下演进过程:
Model-View-Controller] --> MVP[MVP
Model-View-Presentation] MVP --> MVVM[MVVM
Model-View-ViewModel] style MVC fill:#fce4ec,stroke:#c2185b style MVP fill:#fff4e6,stroke:#f57c00 style MVVM fill:#e8f5e9,stroke:#388e3c
为什么要这样演变?
| 架构模式 | 核心问题 | 演进动机 |
|---|---|---|
| MVC (Model-View-Controller) |
|
需要更好的关注点分离和可测试性 |
| MVP (Model-View-Presenter) |
|
需要自动化的数据绑定和响应式更新 |
| MVVM (Model-View-ViewModel) |
|
响应式编程的最佳实践 |
演进的本质:逐步解耦和自动化
- MVC → MVP:解决 View 和 Model 的耦合
- 引入 Presenter 作为中介,View 不再直接访问 Model
- View 和 Presenter 通过接口通信,提升可测试性
- 缺点:手动更新 UI 的样板代码过多
- MVP → MVVM:引入数据绑定,实现自动化
- ViewModel 暴露可观察的状态(State)
- View 通过数据绑定自动订阅状态变化
- 状态更新时,UI 自动刷新,无需手动调用
- 优势:代码更简洁,逻辑更清晰,易于维护
前提条件
状态管理方式高度依赖官方 SDK 的支持与否才可以实现。如果官方 SDK 不支持,某些框架将无法实现。
实例:
- 2012-2019年:Android 开发只支持 MVC 状态管理,无法使用 MVVM
- 2020年:Android 官方推出了 BindingView 的 SDK,此后才可以使用 MVVM 做开发
- Flutter:从一开始就支持响应式框架,天然适合 MVVM
技术栈规定
技术栈升级要求
为保证架构的现代化和统一性,必须采用以下技术栈:
| 平台 | UI 框架 | 状态管理 | 说明 |
|---|---|---|---|
| iOS | SwiftUI | Combine + Observation | Apple 官方声明式 UI + 响应式框架 |
| Android | Jetpack Compose | Flow + LiveData | Google 官方声明式 UI + 响应式框架 |
| Flutter | Widget | Riverpod | 跨平台声明式 UI + 现代状态管理 |
为什么强制使用这些技术栈?
- 统一的架构思想:三端都采用声明式 UI + 响应式状态管理,降低学习成本
- 官方推荐方案:SwiftUI、Compose 分别是 Apple 和 Google 的官方推荐技术栈
- 现代化开发:摒弃 UIKit/XML 等过时技术,拥抱声明式编程范式
- 更好的性能:声明式 UI 框架在渲染性能和内存管理上都更优秀
- 易于维护:代码更简洁、逻辑更清晰、bug 更少
- 团队协作:统一技术栈降低沟通成本,提高开发效率
学习资源
iOS - SwiftUI + Combine + Observation:
- SwiftUI 官方教程 - Apple 官方 SwiftUI 完整学习路径
- SwiftUI 官方文档 - SwiftUI 完整 API 文档
- Combine 官方文档 - Apple 响应式框架完整文档
- Observation 官方文档 - Swift 现代化可观察对象框架(iOS 17+)
Android - Jetpack Compose + Flow + LiveData:
- Jetpack Compose 官方课程 - Google 官方 Compose 学习路径
- Jetpack Compose 官方文档 - Compose 完整文档
- Kotlin Flow 官方文档 - Kotlin 协程和 Flow 完整指南
- LiveData 官方文档 - LiveData 使用指南
参考学习链接
深入了解 Flutter 应用架构和 MVVM 模式:
- Flutter 官方架构指南 - Flutter 官方推荐的应用架构设计指南
- Riverpod 官方文档 - Riverpod 状态管理完整学习指南
- Riverpod 官网 - 了解 Riverpod 的核心特性和优势
为什么选择 Riverpod?
本项目使用 Riverpod 作为状态管理方案,基于以下核心技术优势和实践教训:
1. 性能优化机制
刷新颗粒度(Rebuild Granularity)
GetX + Obx 的问题:
- Obx 包裹的整个 Widget 都会重建
- 嵌套 Obx 会导致多次重建
- 无法精确控制重建范围
实际案例:
// 整个 Container 都会重建
Obx(() => Container(
height: controller.height.value, // 高度变化
child: Column(
children: [
Text(controller.title.value), // title 变化也会重建整个 Container
Text(controller.content.value), // content 变化也会重建整个 Container
],
),
))
Riverpod 的精细化刷新优化:
| 优化技术 | 作用 | 使用场景 |
|---|---|---|
| select | 精确订阅状态的某个字段 | 只关心状态中的部分数据,避免整个对象变化导致重建 |
| Consumer | 局部重建 Widget 树的一部分 | 只重建需要响应状态变化的最小 Widget 子树 |
| RepaintBoundary | 隔离重绘边界 | 阻止父级重绘影响子级,或子级重绘影响父级 |
| autoDispose | 自动释放不再使用的 Provider | 页面销毁时自动清理资源,避免内存泄漏 |
| family | 为不同参数创建独立 Provider 实例 | 列表中每个 Item 有独立状态,互不影响 |
代码示例
// 1. select:只订阅 title 字段,content 变化时不重建
final title = ref.watch(chatViewModelProvider.select((s) => s.title));
// 2. Consumer:只重建 Consumer 包裹的部分
Widget build(BuildContext context) {
return Column(
children: [
Text('静态内容,永远不重建'),
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('动态内容: $count'); // 只有这里会重建
},
),
Text('静态内容,永远不重建'),
],
);
}
// 3. RepaintBoundary:隔离复杂动画,避免影响其他 Widget
RepaintBoundary(
child: ComplexAnimationWidget(), // 动画重绘不会影响外部
)
// 4. autoDispose:页面销毁时自动释放资源
final userProvider = StateNotifierProvider.autoDispose<UserViewModel, UserState>(
(ref) => UserViewModel(),
);
// 5. family:列表中每个 Item 有独立状态
final messageProvider = Provider.family<Message, int>((ref, messageId) {
return ref.watch(chatRepositoryProvider).getMessageById(messageId);
});
性能对比:
| 场景 | 不使用优化 | 使用优化 | 提升 |
|---|---|---|---|
| 聊天列表滚动 | 所有 Item 重建 | 只重建可见 Item | 性能显著提升 |
| 状态局部更新 | 整个页面重建 | 只重建 Consumer 部分 | 大幅减少 rebuild |
| 动画播放 | 影响整个 Widget 树 | RepaintBoundary 隔离 | 流畅度明显提升 |
| 列表 Item 状态 | 共享状态,互相影响 | family 独立状态 | 避免无关重建 |
性能提升效果:
- 大幅减少不必要的 rebuild
- 明显降低 CPU 使用
- 显著提升滑动流畅度
2. 单向数据流(Unidirectional Data Flow)
为什么需要单向数据流?
GetX 的双向绑定问题:
// GetX 允许在任何地方修改状态,导致数据流混乱
class ChatPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find<ChatController>();
return Column(
children: [
// 问题1:UI 直接修改状态
ElevatedButton(
onPressed: () => controller.isLoading.value = true, // UI 直接改状态
),
// 问题2:不知道状态从哪里来,往哪里去
Obx(() => Text(controller.message.value)),
],
);
}
}
数据流混乱导致:
- 无法追踪状态变化来源
- 多处修改同一状态,冲突频发
- Debug 困难,不知道谁改了状态
Riverpod 的单向数据流:
用户操作 → ViewModel 方法 → 更新 State → UI 自动响应
↑ ↓
└─────────────── 只读数据 ──────────────────┘
代码实现:
class ChatPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 只读:只能读取状态,不能直接修改
final state = ref.watch(chatViewModelProvider);
final viewModel = ref.read(chatViewModelProvider.notifier);
return Column(
children: [
// 清晰:通过 ViewModel 方法修改状态
ElevatedButton(
onPressed: () => viewModel.sendMessage('hello'),
),
// 响应:UI 自动响应状态变化
Text(state.message),
],
);
}
}
优势:
- 数据流向清晰:用户操作 → ViewModel → State → UI
- 状态只有一个修改入口:ViewModel 的方法
- 易于追踪:DevTools 可以看到完整的状态变化历史
- 状态历史回溯:可以回退查看之前任意时刻的应用状态,像看视频回放一样
3. 编译期类型安全(Compile-time Safety)
GetX 的运行时陷阱
问题1:字符串查找 Controller
// 编译通过,运行时崩溃
final controller = Get.find<ChatController>(); // 如果忘记 Get.put,运行时炸
问题2:依赖关系不明确
class ChatController extends GetxController {
void sendMessage() {
// 不知道依赖了什么,运行时才知道
objectMgr.chatMgr.send(); // objectMgr 是什么?从哪来?
}
}
问题3:类型不安全
// 编译通过,运行时类型错误
Get.put<BaseController>(ChatController());
final controller = Get.find<UserController>(); // 找到 ChatController,类型不匹配!
运行时错误案例:
- Controller 未初始化
- 类型转换错误
- 依赖循环引用
Riverpod 的编译期保证
保证1:依赖必须存在
@riverpod
class ChatViewModel extends _$ChatViewModel {
@override
ChatState build() {
// 编译期就知道依赖了什么
final chatRepo = ref.watch(chatRepositoryProvider); // 如果 Provider 不存在,编译报错
return const ChatState();
}
}
保证2:类型完全正确
// 类型由编译器推断,完全正确
final state = ref.watch(chatViewModelProvider); // state 类型是 ChatState
final viewModel = ref.read(chatViewModelProvider.notifier); // viewModel 类型是 ChatViewModel
保证3:依赖图可视化
// Riverpod DevTools 可以看到完整的依赖图
ChatViewModel
├─ chatRepositoryProvider
│ ├─ networkSdkApiProvider
│ └─ messageLocalDataSourceProvider
└─ sendMessageUseCaseProvider
└─ chatRepositoryProvider
编译期保证的价值:
- 大幅减少 Bug:运行时错误显著下降
- 提升开发效率:不需要运行才知道对错
- 重构安全:IDE 自动提示依赖变化
- 团队协作:依赖关系明确,不会互相影响
GetX + Obx vs Riverpod:实践教训
典型问题示例
以下内容展示了 GetX + Obx 在大型项目中暴露的典型问题,作为选型参考。
1. 状态管理混乱
GetX + Obx 问题代码示例(chat_list_controller.dart):
class ChatListController extends GetxController {
var lastClickedSpecialChatId = (-1).obs;
final chatList = <Chat>[].obs;
final companyChatList = <Chat>[].obs;
final lockedChatList = <Chat>[].obs;
final RxBool isNavigating = false.obs;
final isInitializing = true.obs;
ValueNotifier<bool> isInitializingValue = ValueNotifier(true); // 混用!
final isShowSkeleton = false.obs;
ValueNotifier<double> offset = ValueNotifier(0); // 混用!
RxDouble offsetObx = 0.0.obs; // 重复状态!
RxDouble miniAppIconOpacityWhenShowingMiniApp = 1.0.obs;
RxDouble miniAppIconOpacityWhenShowingChatView = 0.01.obs;
RxDouble miniAppIconScale = 0.5.obs;
RxBool isShowingApplet = false.obs;
RxBool isMiniAppletDraggingIcon = false.obs;
RxBool isBeingHovered = false.obs;
RxBool isShowDeleteBar = true.obs;
// ... 还有 50+ 个响应式变量!
}
问题分析:
- 状态零散:50+ 个独立的
.obs变量,没有统一结构 - 混合使用:
RxBool,RxDouble,ValueNotifier混杂 - 重复状态:
offset和offsetObx表示同一个东西 - 命名混乱:英文、拼音、中文注释混杂
- UI 状态污染:动画、透明度、偏移量都混在 Controller 里
2. 过度嵌套和性能问题
Obx 嵌套地狱示例(home_view.dart):
body: Obx(() => AnimatedPadding(
child: GetBuilder(
builder: (_) => Obx(
() => Stack(
children: [
Obx(
() => Container(
child: Obx(
() => Opacity(
opacity: controller.opacity.value,
child: Obx(
() => AnimatedContainer(...), // 第6层 Obx!
),
),
),
),
),
],
),
),
),
)),
问题分析:
- 嵌套地狱:单个文件 12 个 Obx,最深 6 层嵌套
- 过度重建:每个 Obx 独立监听,导致大量不必要的 rebuild
- Obx + GetBuilder 混用:逻辑混乱,性能更差
- 难以调试:无法追踪哪个状态导致了 rebuild
3. Controller 过于臃肿
巨型 Controller 反模式示例:
// chat_list_controller.dart
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:bot_toast/bot_toast.dart';
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
// ... 共 77 个 import!
class ChatListController extends GetxController
with GetTickerProviderStateMixin,
WidgetsBindingObserver,
LockedOverlayMixin { // 多个 Mixin 混杂
// UI 逻辑
void handleScroll() { ... }
void animateOpacity() { ... }
// 业务逻辑
Future<void> loadChats() { ... }
Future<void> sendMessage() { ... }
// 动画逻辑
void startAnimation() { ... }
late AnimationController animController;
// 路由逻辑
void navigateToChat() { ... }
// ... 几千行代码全在一个文件!
}
问题分析:
- 77 个 import:依赖关系复杂,难以维护
- 职责不清:UI、业务、动画、路由全混在一起
- 文件巨大:单个 Controller 文件几千行
- 难以复用:逻辑耦合,无法独立使用
4. 没有编译时安全
GetX 运行时陷阱:
// 通过字符串查找 Controller,运行时才知道对错
Get.find<ChatListController>();
Get.put(ChatListController()); // 全局单例,容易冲突
// 依赖注入不明确,不知道依赖了什么
class ChatListController extends GetxController {
void loadData() {
// 直接访问全局 Manager,依赖不透明
objectMgr.chatMgr.getChats();
objectMgr.groupMgr.getGroups();
// 不知道这个 Controller 到底依赖了什么
}
}
问题分析:
- 运行时错误:编译通过,运行时才崩溃
- 全局污染:
Get.find全局查找,容易命名冲突 - 依赖不明:不知道 Controller 依赖了哪些服务
- 难以重构:改一个地方,不知道影响范围
5. 难以测试
GetX 测试困境:
// 测试时必须初始化整个 GetX 框架
testWidgets('test chat list', (tester) async {
// 必须先初始化所有依赖
Get.put(ObjectMgr());
Get.put(ChatMgr());
Get.put(GroupMgr());
// ... 几十个依赖
final controller = Get.put(ChatListController());
// 无法 mock,因为 Controller 直接依赖全局对象
// objectMgr.chatMgr 是全局单例,无法替换
});
问题分析:
- 测试困难:需要初始化整个 GetX 生态
- 无法 Mock:依赖全局对象,无法注入 mock
- 测试耦合:测试依赖 GetX 框架
- 覆盖率低:太难测,团队放弃测试
GetX + Obx vs Riverpod 实际对比
| 技术维度 | GetX + Obx | Riverpod | 技术优势 |
|---|---|---|---|
| 刷新颗粒度 | Obx 包裹的整个 Widget 无法精确控制范围 |
精确依赖追踪 支持 select 细粒度订阅 |
大幅减少不必要 rebuild |
| 性能基准 | 状态更新较慢 内存占用较高 |
状态更新快速 内存占用较低 |
性能明显提升 内存显著减少 |
| 数据流 | 双向绑定 状态可在任意位置修改 |
单向数据流 状态只能通过 ViewModel 修改 |
数据流清晰可追踪 便于状态历史回溯 |
| 编译期安全 | 运行时查找 Controller 类型错误运行时才发现 |
编译期类型检查 依赖不存在立即报错 |
大幅减少 Bug 提升开发效率 |
| 代码生成 | 不支持 | riverpod_generator freezed 全面支持 |
大幅减少样板代码 显著提升开发效率 |
| 依赖管理 | 全局 Get.put/find 依赖关系不透明 |
Provider 依赖图 编译期验证依赖 |
依赖关系可视化 重构安全有保障 |
| 测试支持 | 需要初始化 GetX 框架 无法 Mock 全局依赖 |
Provider 可轻松覆盖 纯函数易于测试 |
测试更容易 运行速度更快 |
| DevTools | 基础日志 | 完整依赖图 状态历史回溯 性能分析 |
Debug 效率显著提升 |
真实案例对比
GetX + Obx
状态混乱:
class ChatListController extends GetxController {
final chatList = <Chat>[].obs;
final isLoading = false.obs;
ValueNotifier<double> offset = ValueNotifier(0); // 混用!
RxDouble offsetObx = 0.0.obs; // 重复!
RxBool isShowingApplet = false.obs;
RxBool isMiniAppletDraggingIcon = false.obs;
// ... 50+ 个零散变量
void loadChats() {
objectMgr.chatMgr.getChats(); // 全局依赖,不可测
}
}
UI 嵌套地狱:
Obx(() => GetBuilder(
builder: (_) => Obx(
() => Obx(
() => Obx(() => ...), // 6 层嵌套!
),
),
))
Riverpod(新架构)
状态清晰:
@freezed
class ChatListState with _$ChatListState {
const factory ChatListState({
@Default([]) List<Chat> chats,
@Default(false) bool isLoading,
}) = _ChatListState;
}
class ChatListViewModel extends StateNotifier<ChatListState> {
ChatListViewModel(this._chatRepository)
: super(const ChatListState());
final ChatRepository _chatRepository; // 依赖明确
Future<void> loadChats() async {
state = state.copyWith(isLoading: true);
final chats = await _chatRepository.getChats();
state = state.copyWith(chats: chats, isLoading: false);
}
}
UI 清晰:
class ChatListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(chatListViewModelProvider);
return state.isLoading
? LoadingWidget()
: ChatListView(chats: state.chats);
}
}
GetX + Obx 痛点总结
从实践经验中,我们总结出 GetX + Obx 的核心问题:
- "快速开发"变成"技术债":初期确实快,但 6 个月后代码无法维护
- "响应式"变成"性能杀手":Obx 嵌套导致过度重建,页面卡顿
- "灵活"变成"混乱":没有约束,代码风格千差万别
- "全局管理"变成"依赖噩梦":牵一发动全身,不敢重构
- "简单上手"变成"难以精通":团队成员写出的代码质量参差不齐
Riverpod 的技术优势
基于以上实践教训,我们选择 Riverpod 作为新架构的状态管理方案,因为它从根本上解决了 GetX 的所有问题:
- 编译时安全:不会再有运行时崩溃
- 结构化状态:@freezed 强制统一状态结构
- 精确重建:依赖追踪精确,性能优异
- 职责清晰:ViewModel、Repository、Service 分离明确
- 易于测试:Provider 可轻松覆盖和 Mock
- 团队规范:代码风格统一,质量可控
Riverpod vs Provider 对比
| 特性 | Provider | Riverpod |
|---|---|---|
| 编译时安全 | 不支持 | 支持 |
| 需要 BuildContext | 需要 | 不需要 |
| 代码生成 | 不支持 | 支持 |
| 测试友好 | 一般 | 优秀 |
| 性能 | 好 | 更好 |
| 学习曲线 | 平缓 | 稍陡 |
MVVM + Riverpod 优势
- 清晰的数据流动:数据流向明确,易于追踪
- 响应式更新:UI 与数据自动同步,无需手动刷新
- 状态管理:统一管理应用状态,避免状态混乱
- 可测试性:ViewModel 可独立测试,无需 Widget 环境
- 解耦:View 与 Model 完全分离,业务逻辑独立
- 类型安全:编译时检查,避免运行时错误
Feature 驱动开发
以页面为单位
App 是以页面为导向,设计架构时,必须明确针对平台页面进行开发。每个功能页面独立成一个 Feature。
完整生命周期
每个 Feature 包含完整的生命周期:
用户界面] --> Presentation[Presentation Layer
视图模型] Presentation --> Domain[Domain Layer
业务逻辑] Domain --> Data[Data Layer
数据访问] Data --> Core[Core Layer
应用级基础设施] Data --> SDKs[SDK Packages
packages/*_sdk] style UI fill:#e1f5ff,stroke:#0288d1 style Presentation fill:#fff4e6,stroke:#f57c00 style Domain fill:#f3e5f5,stroke:#7b1fa2 style Data fill:#e8f5e9,stroke:#388e3c style Core fill:#fce4ec,stroke:#c2185b style SDKs fill:#e8f5e9,stroke:#2e7d32
高内聚低耦合
- Feature 之间通过接口通信
- 每个 Feature 可独立开发、测试、部署
- Feature 内部高度内聚,外部低耦合
- 便于团队并行开发
模块设计哲学
核心设计原则
"实现层高度封装,使用侧傻瓜式"
这是本架构所有模块设计遵循的核心哲学:
- 实现层高度封装:将复杂的技术细节、错误处理、类型转换等全部封装在底层
- 使用侧傻瓜式:上层使用者只需关注业务逻辑,无需了解底层实现细节
- 按需使用:提供合理的默认值和可选参数,使用者可以按需定制
网络层设计示例
以网络层为例,展示如何实现"实现层高度封装,使用侧傻瓜式"的设计原则。
1. APIRequestable 协议 - 统一请求接口
设计思想:定义统一的 API 请求协议,所有请求都实现相同的接口
/// API 请求协议 - 所有请求的基础接口
/// parameters 自动序列化
abstract class APIRequestable<T> {
/// API 路径
String get path;
/// HTTP 方法
HTTPMethod get method;
/// 序列化为 JSON(由 @JsonSerializable 自动生成)
Map<String, dynamic> toJson();
/// 自定义请求头
Map<String, String>? get customHeaders => null;
/// 请求类型(决定 header 处理方式)
APIRequestType get requestType => APIRequestType.request;
/// 解码响应(默认实现由扩展提供)
T? decodeResponse(Response response);
}
/// 默认实现 - parameters 自动调用 toJson()
extension APIRequestableDefaults<T> on APIRequestable<T> {
/// 请求参数(自动序列化,用户无需手动定义)
Map<String, dynamic>? get parameters {
// 对于 upload 类型,不序列化参数
if (requestType == APIRequestType.upload) {
return null;
}
// 自动调用 toJson() 序列化请求对象
return toJson();
}
}
核心优势:
- 自动序列化:parameters 自动调用 toJson(),用户无需手动定义
- 统一接口:所有 API 请求都实现同一个协议
- 类型安全:泛型 T 指定响应数据类型
- 注解驱动:通过注解自动生成代码
2. 统一执行入口 - executeRequest
设计思想:提供唯一的请求执行入口,自动处理所有技术细节
/// 执行 API 请求 - 唯一的请求入口
Future<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
final client = ref.read(networkSdkApiProvider);
final config = ref.read(apiConfigProvider);
// 1. 检查网络连接
if (!networkManager.isNetworkAvailable) {
throw const APIError.noNetworkConnection();
}
try {
// 2. 根据请求类型构建 header
final headers = configNotifier.defaultHeaders(
customHeaders: request.customHeaders,
includeToken: request.requestType != APIRequestType.login,
);
// 3. 执行请求
final response = await dio.request(
'${config.baseURL}${request.path}',
data: request.parameters,
options: Options(method: request.method.value, headers: headers),
);
// 4. 自动解码响应
return request.decodeResponse(response);
} on DioException catch (e) {
// 5. 统一错误处理
throw _handleDioError(e);
}
}
封装的技术细节:
- 网络可用性检查
- 请求头自动构建(Token、Content-Type 等)
- URL 拼接
- 响应自动解码
- 错误统一处理和转换
3. 自动响应解码 - 注册机制
设计思想:通过注册机制,自动查找 fromJson 函数,实现响应的自动解码
/// 响应类型注册表
final _fromJsonRegistry = <Type, Function>{};
/// 注册响应类型 - 一次注册,全局可用
T Function(Map<String, dynamic>)? registerResponse<T>(
T Function(Map<String, dynamic>) fromJson,
) {
_fromJsonRegistry[T] = fromJson;
return fromJson;
}
/// 自动解码扩展 - 使用侧无需关心
extension APIRequestableExtension<T> on APIRequestable<T> {
T? decodeResponse(Response response) {
final data = response.data as Map<String, dynamic>;
// 从注册表查找 fromJson 函数
final fromJsonFunc = _fromJsonRegistry[T] as T Function(Map<String, dynamic>)?;
if (fromJsonFunc == null) {
throw StateError('fromJson not registered for type $T');
}
// 自动解码 APIResponseWrapper
final wrapper = APIResponseWrapper<T>.fromJson(
data,
(json) => fromJsonFunc(json as Map<String, dynamic>),
);
// 检查业务错误码
if (wrapper.code != 0) {
throw APIError.apiError(code: wrapper.code, message: wrapper.message);
}
return wrapper.data;
}
}
使用示例:
一个端点 = 一个文件(data/remote/login_request.dart),Response DTO + Request 放在同一文件中。
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
part 'login_request.g.dart';
// ── Response DTO ──
@JsonSerializable()
class LoginData {
final String token;
@JsonKey(name: 'user_id')
final String userId;
final String email;
const LoginData({required this.token, required this.userId, required this.email});
factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
}
// ── Request(零样板:只需 @ApiRequest,无需 @JsonSerializable / fromJson / toJson)──
// @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册
@ApiRequest(
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
method: HttpMethod.post,
responseType: LoginData,
requestType: ApiRequestType.login,
)
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
final String email;
final String password;
LoginRequest({required this.email, required this.password});
// 完毕!toJson 由 mixin 从类字段自动生成,fromJson 不需要(Request 永远手动构造)
}
// 使用 - 超级简单!
final loginData = await apiClient.executeRequest(
LoginRequest(email: 'user@example.com', password: '123456'),
);
final user = loginData?.toEntity(); // DTO → Domain Entity
设计思想对比
| 方案 | 需要定义的内容 | 自动生成 | 维护成本 |
|---|---|---|---|
| 手动方式 | 字段 + 构造函数 + extends + path + method + toJson + parameters + registerResponse | 无 | 高 |
| JsonSerializable | 字段 + 构造函数 + extends + path + method | toJson / fromJson | 中 |
| @ApiRequest(当前方案) | 字段 + 构造函数 + @ApiRequest | path / method / requestType / includeToken / toJson / fromJson 注册 | 极低 |
核心优势:
- 注解驱动:
@ApiRequest一个注解自动生成 mixin(含 toJson),无需@JsonSerializable - 自动注册:fromJson 在首次请求时自动注册到全局注册表,无需手动
registerApiResponses() - 一个端点 = 一个文件:Response DTO + Request 放在同一文件,打开即看全貌
- 傻瓜式使用:使用者只需关注业务字段和注解配置
- 类型安全:
ApiRequestable<T>泛型 +responseType编译期检查
跨平台对比:
| 平台 | 代码示例 | 简洁度 |
|---|---|---|
| Swift | struct LoginRequest: APIRequestable { typealias Response = LoginData ... } |
协议直接实现,最简洁 |
| Dart | @ApiRequest(...) class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi { ... } |
注解 + 代码生成,接近 Swift 体验 |
Dart 通过注解 + 代码生成弥补语言层面没有协议默认实现的不足,达到接近 Swift 的简洁度。
4. 注解定义与代码生成器
设计思想:通过注解 + 代码生成器,自动生成所有技术代码
4.1 注解定义
文件:packages/networks_sdk/lib/src/annotations/api_request.dart
/// API 请求注解 - 标记一个类为 API 请求
///
/// 代码生成器会自动生成 mixin `_$Api`,提供:
/// - path / method / requestType / includeToken 协议实现
/// - 自动注册 responseType 的 fromJson(在 parameters getter 中触发)
class ApiRequest {
/// API 路径(如 '/auth/login')
final String path;
/// HTTP 方法(默认 POST)
final HttpMethod method;
/// 响应类型(用于泛型绑定 + 自动注册 fromJson)
final Type responseType;
/// 请求类型(决定 header 处理方式)
final ApiRequestType requestType;
/// 是否携带 Token(默认根据 requestType 推断:login → false,其余 → true)
final bool? includeToken;
/// 自定义请求头
final Map<String, String>? customHeaders;
const ApiRequest({
required this.path,
this.method = HttpMethod.post,
required this.responseType,
this.requestType = ApiRequestType.request,
this.includeToken,
this.customHeaders,
});
}
4.2 代码生成器核心逻辑
文件:packages/networks_sdk/lib/src/generator/api_request_generator.dart
生成 mixin(非 extension),因为 mixin 可以 override 基类方法、调用 super,并在 parameters getter 中自动注册 fromJson。
toJson 生成机制:生成器读取类的声明字段(非继承),直接在 mixin 中生成 Map 字面量。不依赖 @JsonSerializable,避免了继承属性被序列化导致的递归问题。支持 @JsonKey(name: '...') 字段重命名和 @JsonKey(includeToJson: false) 跳过字段。
/// API 请求代码生成器
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
@override
String generateForAnnotatedElement(element, annotation, buildStep) {
final className = element.name;
// ... 读取 path / method / responseType / requestType / includeToken ...
// 从类的声明字段生成 toJson(),只序列化自身字段,不含继承属性
final toJsonBody = _buildToJsonBody(element, className);
return '''
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@override String get path => '$path';
@override HttpMethod get method => HttpMethod.$methodName;
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
@override bool get includeToken => $includeToken;
@override
Map<String, dynamic> toJson() => $toJsonBody;
@override
Map<String, dynamic>? get parameters {
registerResponse<$responseTypeName>($responseTypeName.fromJson);
return super.parameters;
}
}
''';
}
/// 读取类的声明字段,生成 Map 字面量
/// 支持 @JsonKey(name: '...') 重命名
String _buildToJsonBody(ClassElement element, String className) {
final fields = element.fields.where((f) => !f.isStatic && !f.isSynthetic);
// => {'email': (this as LoginRequest).email, 'password': ...}
}
}
关键设计:
toJson()只序列化类自身声明的字段,不含ApiRequestable的继承属性(path / method / parameters 等),避免递归parametersgetter 在首次请求时自动调用registerResponse,将 Response 的fromJson注册到全局注册表- Upload 等特殊请求在类中 override
toJson(),类的 override 优先于 mixin
4.3 build.yaml 配置
文件:packages/networks_sdk/build.yaml
使用 SharedPartBuilder,与 @JsonSerializable(Response DTO 用)共享同一个 .g.dart 文件,无需额外 part 指令。
builders:
api_request:
import: "package:networks_sdk/src/generator/builder.dart"
builder_factories: ["apiRequestBuilder"]
build_extensions: {".dart": [".api_request.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
4.4 运行命令
⚠️ 强制要求:开发期间必须常驻 watch 模式
# 在项目根目录打开一个独立终端窗口,执行(整个开发期间只需一次):
melos run gen:watch
启动后,每次保存 .dart 文件都会自动重新生成 .g.dart。
手写代码时 IDE 报红是正常的,保存后红线自动消失。
# 仅在 watch 出问题时使用:全量重新生成
melos run gen
4.5 更多使用示例
所有示例遵循同一模式:@ApiRequest + extends ApiRequestable<T> with _$XxxApi。Request 类无需 @JsonSerializable。
发送消息请求(POST + @JsonKey 字段重命名):
// data/remote/send_message_request.dart
// ── Response DTO(仍用 @JsonSerializable)──
@JsonSerializable()
class SendMessageData {
@JsonKey(name: 'message_id')
final String messageId;
final int timestamp;
const SendMessageData({required this.messageId, required this.timestamp});
factory SendMessageData.fromJson(Map<String, dynamic> json) =>
_$SendMessageDataFromJson(json);
}
// ── Request(零样板)──
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
class SendMessageRequest extends ApiRequestable<SendMessageData>
with _$SendMessageRequestApi {
@JsonKey(name: 'chat_id') // 生成器会读取,JSON 键名为 'chat_id'
final String chatId;
final String content;
SendMessageRequest({required this.chatId, required this.content});
// toJson 自动生成:{'chat_id': chatId, 'content': content}
}
获取用户资料(GET,靠 token 标识当前用户,无需传参):
// data/remote/get_profile_request.dart
@JsonSerializable(createToJson: false) // 只需反序列化
class ProfileData {
@JsonKey(name: 'user_id')
final String userId;
final String email;
final String? nickname;
final String? avatar;
const ProfileData({required this.userId, required this.email, this.nickname, this.avatar});
factory ProfileData.fromJson(Map<String, dynamic> json) =>
_$ProfileDataFromJson(json);
User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
}
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
class GetProfileRequest extends ApiRequestable<ProfileData>
with _$GetProfileRequestApi {
GetProfileRequest(); // 无参数 — toJson 自动生成空 map
}
上传文件请求(FormData multipart):
// data/remote/upload_file_request.dart
@JsonSerializable()
class UploadResult {
final String url;
@JsonKey(name: 'file_id')
final String fileId;
const UploadResult({required this.url, required this.fileId});
factory UploadResult.fromJson(Map<String, dynamic> json) =>
_$UploadResultFromJson(json);
}
@ApiRequest(
path: ApiPaths.uploadFile,
method: HttpMethod.post,
responseType: UploadResult,
requestType: ApiRequestType.upload,
)
class UploadFileRequest extends ApiRequestable<UploadResult>
with _$UploadFileRequestApi {
final String filePath;
final String? fileName;
UploadFileRequest({required this.filePath, this.fileName});
@override
Map<String, dynamic> toJson() => {}; // upload 不走 toJson
@override
Object? get uploadData => FormData.fromMap({
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
});
}
核心价值
- 极简使用:字段 + 构造函数 +
@ApiRequest(Request 无需@JsonSerializable、无需fromJson、无需手写toJson) - 零维护:path / method / requestType / includeToken / toJson / fromJson 注册 全部自动生成
- 类型安全:泛型
ApiRequestable<T>+responseType编译期检查 - 一个端点 = 一个文件:Response DTO + Request 放在同一文件,打开即看全貌
5. Riverpod 集成 - 依赖注入
设计思想:Network SDK 本身零 Flutter / 零 Riverpod 依赖。App 层通过 Provider 包装,实现全局单例 + 依赖注入。
DI 装配:
// ── app/di/network_provider.dart ── (SDK 基础设施,全局唯一)
/// 1. API 配置(baseURL 来自 config.json → --dart-define-from-file)
final apiConfigProvider = Provider<ApiConfig>((ref) {
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {'Platform': 'Android', 'client-version': '1.0.0'},
tokenExpiredCodes: {30002, 30003, 30124},
forceLogoutCodes: {30125},
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
onTokenRefresh: () async { /* 刷新 token */ return null; },
onLog: (message, {tag}) { print('[$tag] $message'); },
);
});
/// 2. Networks SDK API Provider(全局单例,Facade 接口)
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
final config = ref.read(apiConfigProvider);
return NetworksSdkWiring.build(config: config);
});
// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)
/// 3. Repository(注入 Facade 接口类型,ViewModel 不感知具体实现)
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final apiConfig = ref.read(apiConfigProvider);
return AuthRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存(networks_sdk)
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
},
);
});
/// 4. UseCase(按需)
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});
ViewModel 中使用:
@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() => const LoginState();
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true);
try {
// UseCase 封装格式校验 + 业务编排,ViewModel 只需一行
final user = await ref.read(loginUseCaseProvider).execute(
email: email, password: password);
state = state.copyWith(user: user, isLoading: false);
} on FormatException catch (e) {
// 格式校验失败(UseCase 层抛出)
state = state.copyWith(error: e.message, isLoading: false);
} on ApiError catch (e) {
// 统一错误处理 - Freezed union type
state = state.copyWith(error: e.displayMessage, isLoading: false);
}
}
}
完整装配链路:
View: ref.watch(loginViewModelProvider)
→ ViewModel: ref.read(loginUseCaseProvider).execute(...)
→ LoginUseCase: 格式校验(邮箱 + 密码)
→ LoginUseCase: authRepository.login(...)
→ Repository: _client.executeRequest(LoginRequest(...))
→ ApiClient.executeRequest() ← networks_sdk 内部
→ AuthInterceptor ← 注入 token + headers
→ EncryptionInterceptor ← 加密请求体(预留)
→ Dio.request(baseURL + path, data) ← 实际 HTTP 请求
→ RetryInterceptor ← token 过期自动刷新重试 + 业务错误钩子
→ LoggingInterceptor ← 请求/响应日志
← request.decodeResponse(response) ← 自动解码
← ApiResponseWrapper.fromJson ← 拆 { code, msg, data }
← fromJsonRegistry[LoginData] ← 查注册表
← LoginData.fromJson(data) ← 反序列化
← LoginData(DTO)
→ onTokenUpdate(token) ← 回调写入 Token(内存 + 持久化)
← loginData.toEntity() → User ← DTO → Domain Entity
← User
← state.copyWith(user: user) ← 更新状态
View: ref.watch → 自动 rebuild ← UI 刷新
设计哲学在本架构中的体现
| 层级 | 实现层(高度封装) | 使用侧(傻瓜式) |
|---|---|---|
| 网络层 | executeRequest 封装所有细节 自动 header、解码、错误处理 |
定义 Request 类 只需 3 个字段:path、method、parameters |
| 数据层 | Repository 封装数据源切换 自动缓存、错误转换 |
调用 Repository 方法 返回 Domain 模型,无需关心数据来源 |
| 业务层 | UseCase 封装业务逻辑 单一职责,可组合 |
ViewModel 调用 UseCase 专注状态管理,不关心业务细节 |
| UI层 | ViewModel 提供响应式状态 自动重建、错误处理 |
Widget 监听 Provider 只需 ref.watch,无需手动管理状态 |
核心优势
- 降低认知负担:使用者无需了解底层实现,专注业务逻辑
- 减少重复代码:通用逻辑封装在底层,上层不重复
- 提升可维护性:修改底层实现不影响上层代码
- 易于测试:每层职责清晰,可独立 Mock 和测试
- 团队协作:新人快速上手,代码风格统一
设计哲学总结
本架构中的每一个模块都遵循"实现层高度封装,使用侧傻瓜式"的原则:
- 底层模块负责技术复杂性,提供简洁的 API
- 上层模块负责业务逻辑,使用简单的接口
- 通过这种分层,实现关注点分离,让每个开发者专注于自己擅长的领域
第二部分:结构是什么(Structure)- 整体架构
设计原则
1.1 SOLID 原则
| 原则 | 说明 | 体现 |
|---|---|---|
| 单一职责 (SRP) | 一个模块只负责一项职责 | 每个 UseCase 只处理一个业务场景 |
| 开闭原则 (OCP) | 对扩展开放,对修改关闭 | 通过接口、策略模式实现扩展点 |
| 里氏替换 (LSP) | 子类可替换父类 | 所有 Repository 实现可互换 |
| 接口隔离 (ISP) | 客户端不依赖不需要的接口 | Repository 按功能拆分,不做大而全接口 |
| 依赖倒置 (DIP) | 依赖抽象而非具体实现 | Domain 层定义接口,Data 层实现 |
1.2 分层依赖规则
界面层] Presentation[Presentation Layer
表现层] Domain[Domain Layer
Domain 层] Data[Data Layer
数据层] Core[Core Layer
应用级基础设施] SDKs[SDK Packages
packages/*_sdk] UI -->|依赖| Presentation Presentation -->|依赖| Domain Domain -.定义接口.-> Data Data -->|依赖| Core Data -->|依赖| SDKs style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:2px style Presentation fill:#fff4e6,stroke:#f57c00,stroke-width:2px style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Core fill:#fce4ec,stroke:#c2185b,stroke-width:2px style SDKs fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
依赖方向:单向向下,严格禁止反向依赖和跨层调用
严格规则:
- UI 层只能调用 Presentation 层
- Presentation 层只能调用 Domain 层
- Domain 层定义接口,不依赖具体实现
- Data 层实现 Domain 接口,调用 Core 层和 SDK Packages
- Core 层提供应用级基础设施,SDK Packages 提供可复用技术能力,均不依赖任何上层
1.3 模块化原则
- 高内聚:相关功能聚合在同一模块
- 低耦合:模块间通过接口通信
- 可替换:底层实现可替换,上层不受影响
- 可测试:每层可独立测试
整体架构(3图)
2.1 整体模块图
图表说明
下图展示基于 Feature 驱动的整体模块划分,每个 Feature 包含完整的 UI → Presentation → Domain 层级。
Flutter Plugin] L10nPkg[L10nSDK] Crypto[CryptoSDK
占位] OtherSDK[Media/RTC/Push/Protocol] end subgraph Core[Core Layer - 主 App 内部] subgraph Foundation[core/foundation/ - 应用级基础设施] Utils[Constants / Config / Errors
Logger / Types / Utils / Extensions] end subgraph CoreUI[core/ui/ - UI 基础设施] Base[基础定义] Components[基础组件] Composites[业务组合组件] end end App --> Features Features --> Domain Domain -.定义接口.-> Data Data --> Network Data --> Storage Data --> Packages Features -->|UI 复用| CoreUI Features -->|本地化文案| L10nPkg CoreUI -->|组件内置文案| L10nPkg CoreUI -->|引用| Foundation style App fill:#667eea,stroke:#5568d3,color:#fff style Features fill:#e1f5ff,stroke:#0288d1 style Domain fill:#f3e5f5,stroke:#7b1fa2 style Data fill:#e8f5e9,stroke:#388e3c style Core fill:#f5f5f5,stroke:#9e9e9e style Packages fill:#e8f5e9,stroke:#388e3c style Foundation fill:#fce4ec,stroke:#c2185b style CoreUI fill:#fff4e6,stroke:#f57c00
2.2 整体目录图
图表说明
完整的项目目录结构,展示了 Feature 驱动的组织方式和清晰的层级关系。
lib/
├── main.dart # 应用入口:调用 bootstrap(),不含任何业务逻辑
│
├── app/ # 应用壳(组合根):负责拼装所有模块,禁止在此写业务逻辑
│ ├── app.dart # MaterialApp 根组件 + WidgetsBindingObserver(前后台事件)
│ ├── bootstrap.dart # 启动入口:ProviderScope 包裹 + 依赖初始化
│ │
│ ├── router/ # 路由管理(go_router)
│ │ ├── app_router.dart # routerProvider:StatefulShellRoute + 全局 redirect
│ │ ├── app_route_name.dart # AppRouteName 枚举,路径常量 + fromPath()
│ │ └── guards/
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
│ │
│ └── di/ # 全局 DI — 手动装配的 Provider
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
│ ├── db_provider.dart # StorageSdkApi(注入 AppDatabase factory)
│ └── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier
│
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
│ │
│ ├── app_tab/ # Tab 容器(底部导航栏)
│ │ └── view/
│ │ └── app_tab.dart # StatefulShellRoute 子壳 + 底部导航逻辑
│ │
│ ├── login/ # 登录 ── 已实现
│ │ ├── di/
│ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider
│ │ ├── presentation/
│ │ │ ├── login_view_model.dart # @riverpod ViewModel(生成 login_view_model.g.dart)
│ │ │ └── login_state.dart # @freezed State(生成 login_state.freezed.dart)
│ │ ├── usecases/
│ │ │ └── login_usecase.dart # 格式校验 → Repository → User Entity
│ │ └── view/
│ │ └── login_page.dart # 登录页
│ │
│ ├── chat/ # 聊天 ── 开发中
│ │ ├── presentation/
│ │ │ └── chat_view_model.dart # @riverpod ViewModel
│ │ └── view/
│ │ ├── chat_page.dart # 会话列表页(Tab 1)
│ │ └── chat_detail_page.dart # 聊天详情页
│ │
│ ├── contact/ # 通讯录 ── 骨架
│ │ └── view/
│ │ └── contact_page.dart # 通讯录页(Tab 2)
│ │
│ └── settings/ # 设置 ── 已实现(主题切换)
│ ├── di/
│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入)
│ ├── presentation/
│ │ ├── settings_view_model.dart # @riverpod ViewModel(设置页导航)
│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart)
│ ├── usecases/
│ │ └── set_theme_usecase.dart # 主题切换用例
│ └── view/
│ ├── settings_page.dart # 设置主页(Tab 3)
│ ├── theme_view.dart # 主题选择页
│ └── widgets/
│ ├── settings_section_header.dart
│ └── theme_option_tile.dart
│
├── domain/ # Domain 层(纯 Dart,零 Flutter / 零网络依赖)
│ ├── entities/
│ │ └── user.dart # 用户实体
│ │ # message / conversation / contact 待开发
│ └── repositories/
│ └── auth_repository.dart # abstract interface
│ # message / chat / contact_repository 待开发
│
├── data/ # Data 层(implements domain 接口)
│ ├── repositories/
│ │ └── auth_repository_impl.dart # 认证仓库
│ │ # message / chat / contact 待开发
│ ├── local/
│ │ └── drift/ # Drift 本地数据库
│ │ ├── app_database.dart # @DriftDatabase 定义 + onUpgrade 自动补列
│ │ # database_connection.dart 已迁移至 storage_sdk(数据库生命周期统一在 SDK 层管理)
│ │ ├── mapper/
│ │ │ └── drift_path_mapper.dart # Drift 路径映射工具
│ │ └── tables/
│ │ └── users.dart # Users 表定义
│ ├── remote/ # Request 文件(一个端点一个文件)
│ │ ├── login_request.dart # 登录
│ │ ├── logout_request.dart # 登出
│ │ ├── get_profile_request.dart # 获取用户信息
│ │ └── upload_file_request.dart # 文件上传
│ │ # send_message / 其他业务端点 待开发
│ └── models/ # 持久化 DTO(@JsonSerializable)
│ └── user_dto.dart # 用户持久化 DTO
│ # message / conversation / contact_dto 待开发
│
└── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
├── foundation/ # 基础配置(各为单独文件,非子目录)
│ ├── api_paths.dart # API 路径常量(ApiPaths.authLogin 等)
│ ├── config.dart # 运行时配置(AppConfig,通过 --dart-define-from-file 注入)
│ ├── constants.dart # 全局常量(AppConstants:重试次数 / 退避延迟 / 超时等)
│ ├── errors.dart # 统一异常体系
│ ├── extensions.dart # Dart 扩展方法(String / DateTime / List 等)
│ ├── logger.dart # 日志门面(分级日志,生产环境自动关闭 debug)
│ ├── types.dart # 通用类型(Result<T> / typedefs / Unit)
│ └── utils.dart # 工具函数(纯函数,无副作用)
│
├── services/ # 跨模块服务(有状态,作为独立 Provider)
│ ├── app_initializer.dart # 启动初始化编排(按序初始化各依赖)
│ ├── network_backoff_debouncer.dart # 网络恢复退避防抖(4s→8s→...→60s,2min 重置)
│ ├── network_monitor.dart # 网络状态监听(connectivity_plus)
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
│
└── ui/ # Core UI(设计系统 + 可复用组件)
├── base/ # 设计 Token
│ ├── assets.dart # 静态资源路径常量(AppAssets:logo / 占位图)
│ ├── icons.dart # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态)
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
│ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors)
│ └── font.dart # 字体(TextStyle 定义 + textTheme(brightness))
├── components/ # 原子组件
│ └── app_button.dart # 按钮
│ # app_text_field / app_avatar / app_badge 等 待开发
└── composites/ # 组合组件(目录预留,待开发)
# app_dialog / app_toast / app_empty_state 等
2.3 整体分层图(MVVM + Riverpod 数据流)
图表说明
展示完整的五层架构,标注 MVVM 角色映射和 Riverpod 驱动的单向数据流。
ref.watch(viewModel) 订阅状态"] Widgets["UI Widgets
纯展示组件"] end subgraph Layer4["ViewModel 层(MVVM 的 VM)"] direction TB ViewModels["Notifier<State>
状态管理 + 直接方法调用"] end subgraph Layer3[Domain 层] direction TB UseCases["Use Cases
业务用例"] Entities["Entities
Domain 实体"] RepoInterfaces["Repository 接口
(依赖倒置)"] end subgraph Layer2[Data 层] direction TB RepoImpls[Repository 实现] LocalDS[Local DataSource] DTOs["DTO Models(MVVM 的 M)"] end subgraph Layer1[Core 层 - 主 App 内部] direction TB Foundation[foundation/
Constants/Config/Errors/Logger/Types/Utils] CoreUI[ui/] end subgraph Layer0[SDK Packages - Melos 管理] direction TB SDKPkgs[networks_sdk / storage_sdk / cipher_guard_sdk / l10n_sdk
media_sdk / rtc_sdk / notification_sdk
protocol_sdk] end Pages -->|"① 用户操作 → ref.read(vm.notifier).action()"| ViewModels ViewModels -->|"② 调用业务逻辑"| UseCases UseCases --> RepoInterfaces RepoInterfaces -.实现.-> RepoImpls RepoImpls --> LocalDS RepoImpls --> SDKPkgs LocalDS --> SDKPkgs ViewModels -.->|"③ state = newState"| ViewModels ViewModels -.->|"④ ref.watch 自动触发 rebuild"| Pages style Layer5 fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style Layer4 fill:#fff4e6,stroke:#f57c00,stroke-width:3px style Layer3 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px style Layer2 fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style Layer1 fill:#fce4ec,stroke:#c2185b,stroke-width:3px style Layer0 fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px
2.4 MVVM + Riverpod 数据流映射
目录结构到 MVVM 和 Riverpod 数据流的精确映射:
view/ 目录
ConsumerWidget"] VM["ViewModel
presentation/ 目录
StateNotifier"] M["Model
model/ + domain/entities/
UI Model + Entity"] end subgraph RiverpodFlow["Riverpod 单向数据流"] direction TB Step1["① 用户点击发送按钮"] Step2["② ref.read(chatVM.notifier)
.sendMessage(content)"] Step3["③ ViewModel 调用 UseCase
→ Repository → NetworksSdkApi"] Step4["④ state = state.copyWith(
messages: [..., newMsg])"] Step5["⑤ ref.watch(chatVM) 检测变化
→ ConsumerWidget 自动 rebuild"] Step6["⑥ UI 展示最新消息列表"] end Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> Step6 subgraph DirMapping["目录 ↔ 角色"] direction TB D1["chat_page.dart → View"] D2["chat_view_model.dart → ViewModel"] D5["domain/entities/message.dart → Model"] end style MVVM fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style RiverpodFlow fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style DirMapping fill:#fff4e6,stroke:#f57c00,stroke-width:2px
两大核心逻辑:
1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。
2. Riverpod 单向数据流:用户操作 →
ref.read(vm.notifier).action()→ ViewModel 处理逻辑 →state = newState→ref.watch(vm)检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。3. Widget 纯展示原则:
build()只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。
- 派生显示值必须是 State getter:凡是需要从 State 字段推导出另一个值(文本、颜色、样式等),一律写成 State 的 getter,Widget 只读取结果。例如:
String get buttonLabel => testStarted ? '结束' : '开始';,Widget 写state.buttonLabel,不在build()里写三元。- 禁止
build()内定义局部计算变量:final isSelected = current == mode这类从构造参数/state 派生值的局部变量,不属于build()。组件应接受已算好的bool isSelected参数,由父级或 State getter 负责计算。- 导航、CRUD、状态变更全部在 ViewModel 中完成,Widget 只转发事件:
onTap: () => ref.read(vm.notifier).doAction()。- demo/测试页面同样适用:demo 代码是示范,别人会照着模仿,写法必须与正式页面完全一致。
第三部分:核心概念(Core Concepts)
Riverpod 核心概念
在深入了解各层实现之前,先理解 Riverpod 的核心概念和使用方式。
| 概念 | 说明 | 使用场景 |
|---|---|---|
| StateNotifier | 管理可变状态的类 | ViewModel 实现 |
| StateNotifierProvider | 提供 StateNotifier 的 Provider | ViewModel Provider |
| Provider | 提供不可变对象的 Provider | UseCase、Repository 依赖注入 |
| ConsumerWidget | 可以监听 Provider 的 Widget | UI 层页面组件 |
| WidgetRef | 访问 Provider 的引用 | 在 Widget 中读取和监听 Provider |
| @riverpod | 代码生成注解 | 自动生成 Provider 代码 |
| @freezed | 不可变类注解 | 生成 State 类的 copyWith 等方法 |
| autoDispose | 自动释放 Provider | 页面销毁时自动清理资源 |
Clean Architecture 分层说明
本架构严格遵循 Clean Architecture 的分层原则,每层都有明确的职责和依赖方向。
分层职责
| 层级 | 职责 | 依赖方向 | 示例 |
|---|---|---|---|
| UI Layer | 界面展示、用户交互 | → Presentation | ChatPage、MessageItem Widget |
| Presentation Layer | 状态管理、UI 逻辑 | → Domain | ChatViewModel、MessageState |
| Domain Layer | 业务逻辑、业务规则 | → Repository 接口 | SendMessageUseCase、ChatEntity |
| Data Layer | 数据访问、数据源管理 | → Core | ChatRepository、ChatRepositoryImpl |
| Core Layer | 应用级基础设施(Constants/Config/Errors 等) | 无依赖 | app_config.dart、error_mapper.dart |
| SDK Packages | 可复用技术能力(packages/*_sdk) | 无依赖 | NetworkSDK、StorageSDK、L10nSDK |
| Core UI Layer | 基础定义、基础组件、业务组合组件 | → Core | AppButton、AppDialog、base/colors |
依赖倒置原则(DIP)
核心思想:高层模块不依赖低层模块,两者都依赖抽象(接口)
表现层] UC[UseCase
业务层] RI[Repository Interface
接口定义] RImpl[Repository Impl
数据层实现] VM --> UC UC --> RI RImpl -.实现.-> RI style VM fill:#fff4e6,stroke:#f57c00 style UC fill:#f3e5f5,stroke:#7b1fa2 style RI fill:#e3f2fd,stroke:#0288d1 style RImpl fill:#e8f5e9,stroke:#388e3c
示例:
// Domain 层定义接口
abstract class ChatRepository {
Future<List<Message>> getMessages(String chatId);
Future<void> sendMessage(Message message);
}
// Data 层实现接口
class ChatRepositoryImpl implements ChatRepository {
final NetworksSdkApi _client;
final MessageLocalDataSource _localDataSource;
@override
Future<List<Message>> getMessages(String chatId) async {
// 实现数据获取逻辑
}
}
// Presentation 层使用接口
class ChatViewModel {
final ChatRepository _repository; // 依赖接口,不依赖实现
Future<void> loadMessages() async {
final messages = await _repository.getMessages(chatId);
// ...
}
}
Repository 模式
作用:将数据访问逻辑封装起来,对上层提供统一的数据访问接口
优势:
- 数据源可替换:可以从网络、本地数据库、缓存等任意数据源获取
- 业务逻辑与数据访问分离:业务层不关心数据来自哪里
- 易于测试:可以轻松 Mock Repository
业务逻辑] Repo[Repository
数据仓库] Remote[Remote Data Source
网络数据源] Local[Local Data Source
本地数据源] Cache[Cache
缓存] UC --> Repo Repo --> Remote Repo --> Local Repo --> Cache style UC fill:#f3e5f5,stroke:#7b1fa2 style Repo fill:#e3f2fd,stroke:#0288d1 style Remote fill:#e8f5e9,stroke:#388e3c style Local fill:#fff4e6,stroke:#f57c00 style Cache fill:#fce4ec,stroke:#c2185b
第四部分:怎么做(How)- 详细实现
UI 层模块详解
3.1 UI 层职责
UI 层是应用的最外层,负责:
- 展示用户界面
- 接收用户交互
- 调用 ViewModel 方法
- 监听 ViewModel 状态变化
- 响应式更新 UI
3.1.1 UI 层详细分层结构
UI 层不是单一层级,而是有明确的分层结构:
设计系统] UI --> Foundation[Foundation
基础组件] UI --> Business[Business Components
业务组件] UI --> Pages[Pages
页面] DesignSystem --> Colors[Colors 颜色] DesignSystem --> Typography[Typography 字体] DesignSystem --> Tokens[Design Tokens 基础定义] DesignSystem --> Theme[Theme 主题] Foundation --> Atoms[Atoms 原子组件] Foundation --> Molecules[Molecules 分子组件] Foundation --> Organisms[Organisms 有机组件] Business --> FeatureWidgets[Feature Widgets
功能组件] Pages --> FeaturePages[Feature Pages
功能页面] style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style DesignSystem fill:#fff9c4,stroke:#f57f17,stroke-width:2px style Foundation fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style Business fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Pages fill:#fce4ec,stroke:#c2185b,stroke-width:2px
UI 层分层说明
| 层级 | 职责 | 示例 | 特点 |
|---|---|---|---|
| L1: Design System 设计系统 |
定义应用的视觉语言、颜色、字体、间距等设计规范 | Colors、Typography、Spacing、BorderRadius | 与 Figma 设计稿一一对应 |
| L2: Foundation 基础组件 |
可复用的 UI 基础组件,不包含业务逻辑 | Button、TextField、Card、Avatar | Atomic Design 原则 |
| L3: Business Components 业务组件 |
包含业务逻辑的复用组件 | MessageBubble、ChatItem、ContactCard | Feature 级别复用 |
| L4: Pages 页面 |
完整的页面,组合各种组件 | ChatPage、ChatListPage、ContactPage | Feature 独有 |
L1: Design System(设计系统)
核心原则:与 Figma 设计稿完全对应,确保设计与实现一致。
1.1 Colors(颜色)
/// 颜色定义 - 与 Figma 设计稿对应
class AppColors {
// Primary Colors - 主色
static const primary = Color(0xFF667EEA);
static const primaryDark = Color(0xFF5568D3);
static const primaryLight = Color(0xFF8B9FFF);
// Secondary Colors - 辅助色
static const secondary = Color(0xFF764BA2);
static const secondaryDark = Color(0xFF5E3882);
static const secondaryLight = Color(0xFF9B6FC4);
// Neutral Colors - 中性色
static const black = Color(0xFF000000);
static const white = Color(0xFFFFFFFF);
static const gray900 = Color(0xFF1A1A1A);
static const gray800 = Color(0xFF2D2D2D);
static const gray700 = Color(0xFF404040);
static const gray600 = Color(0xFF5C5C5C);
static const gray500 = Color(0xFF737373);
static const gray400 = Color(0xFF999999);
static const gray300 = Color(0xFFBFBFBF);
static const gray200 = Color(0xFFE6E6E6);
static const gray100 = Color(0xFFF5F5F5);
static const gray50 = Color(0xFFFAFAFA);
// Semantic Colors - 语义色
static const success = Color(0xFF10B981);
static const warning = Color(0xFFF59E0B);
static const error = Color(0xFFEF4444);
static const info = Color(0xFF3B82F6);
}
1.2 Typography(字体)
/// 字体定义 - 与 Figma 设计稿对应
class AppTypography {
// Display - 展示标题
static const displayLarge = TextStyle(
fontSize: 57,
fontWeight: FontWeight.w700,
height: 1.12,
);
static const displayMedium = TextStyle(
fontSize: 45,
fontWeight: FontWeight.w700,
height: 1.16,
);
// Headline - 标题
static const headlineLarge = TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
height: 1.25,
);
static const headlineMedium = TextStyle(
fontSize: 28,
fontWeight: FontWeight.w600,
height: 1.29,
);
// Body - 正文
static const bodyLarge = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.5,
);
static const bodyMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
);
// Label - 标签
static const labelLarge = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.43,
);
static const labelMedium = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.33,
);
}
1.3 Design Tokens(基础定义)
/// 基础定义 - 间距、圆角、阴影等
class AppTokens {
// Spacing - 间距(8pt 网格系统)
static const spacing4 = 4.0;
static const spacing8 = 8.0;
static const spacing12 = 12.0;
static const spacing16 = 16.0;
static const spacing20 = 20.0;
static const spacing24 = 24.0;
static const spacing32 = 32.0;
static const spacing40 = 40.0;
static const spacing48 = 48.0;
// Border Radius - 圆角
static const radiusSmall = 4.0;
static const radiusMedium = 8.0;
static const radiusLarge = 12.0;
static const radiusXLarge = 16.0;
static const radiusFull = 9999.0;
// Elevation - 阴影
static const elevationNone = 0.0;
static const elevationLow = 2.0;
static const elevationMedium = 4.0;
static const elevationHigh = 8.0;
}
1.4 Theme(主题 - 黑暗模式)
/// 主题定义 - 支持亮色/暗色模式
class AppTheme {
// Light Theme
static ThemeData light = ThemeData(
brightness: Brightness.light,
primaryColor: AppColors.primary,
scaffoldBackgroundColor: AppColors.white,
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
secondary: AppColors.secondary,
error: AppColors.error,
surface: AppColors.white,
background: AppColors.gray50,
),
textTheme: TextTheme(
displayLarge: AppTypography.displayLarge,
headlineMedium: AppTypography.headlineMedium,
bodyLarge: AppTypography.bodyLarge,
),
);
// Dark Theme
static ThemeData dark = ThemeData(
brightness: Brightness.dark,
primaryColor: AppColors.primary,
scaffoldBackgroundColor: AppColors.gray900,
colorScheme: const ColorScheme.dark(
primary: AppColors.primary,
secondary: AppColors.secondary,
error: AppColors.error,
surface: AppColors.gray800,
background: AppColors.black,
),
textTheme: TextTheme(
displayLarge: AppTypography.displayLarge.copyWith(color: AppColors.white),
headlineMedium: AppTypography.headlineMedium.copyWith(color: AppColors.white),
bodyLarge: AppTypography.bodyLarge.copyWith(color: AppColors.gray100),
),
);
}
1.5 Figma 设计稿对应规范
重要原则:代码中的命名必须与 Figma 设计稿完全对应
| Figma 命名 | 代码命名 | 说明 |
|---|---|---|
Primary/Main |
AppColors.primary |
主色 |
Button/Primary |
AppButton.primary() |
主按钮 |
Text/Headline/Large |
AppTypography.headlineLarge |
大标题 |
Spacing/16 |
AppTokens.spacing16 |
16pt 间距 |
Radius/Medium |
AppTokens.radiusMedium |
中等圆角 |
好处:
- 设计师与开发者使用相同的术语,沟通零障碍
- 代码审查时可直接对照 Figma 检查实现
- 设计变更时快速定位需要修改的代码
L2: Foundation(基础组件 - Atomic Design)
遵循 Atomic Design 原则,分为三个层级:
2.1 Atoms(原子组件)
最小的 UI 单元,不可再分。
/// AppButton - 按钮原子组件
class AppButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final ButtonVariant variant;
final ButtonSize size;
const AppButton.primary({
required this.text,
this.onPressed,
this.size = ButtonSize.medium,
}) : variant = ButtonVariant.primary;
const AppButton.secondary({
required this.text,
this.onPressed,
this.size = ButtonSize.medium,
}) : variant = ButtonVariant.secondary;
@override
Widget build(BuildContext context) {
// 使用 Design System 的颜色和字体
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: _getBackgroundColor(),
foregroundColor: _getForegroundColor(),
padding: _getPadding(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTokens.radiusMedium),
),
),
child: Text(text, style: _getTextStyle()),
);
}
}
/// AppTextField - 文本框原子组件
class AppTextField extends StatelessWidget {
final String? label;
final String? hint;
final TextEditingController? controller;
const AppTextField({
this.label,
this.hint,
this.controller,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
style: AppTypography.bodyMedium,
decoration: InputDecoration(
labelText: label,
hintText: hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTokens.radiusMedium),
),
),
);
}
}
2.2 Molecules(分子组件)
由多个原子组件组合而成。
/// SearchBar - 搜索栏分子组件
class SearchBar extends StatelessWidget {
final String hint;
final ValueChanged<String>? onChanged;
const SearchBar({
required this.hint,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(AppTokens.spacing12),
child: Row(
children: [
Icon(Icons.search, color: AppColors.gray500),
SizedBox(width: AppTokens.spacing8),
Expanded(
child: AppTextField(
hint: hint,
controller: TextEditingController(),
),
),
],
),
);
}
}
2.3 Organisms(有机组件)
由原子和分子组件组合成的复杂组件。
/// UserCard - 用户卡片有机组件
class UserCard extends StatelessWidget {
final String name;
final String avatar;
final String lastMessage;
final VoidCallback? onTap;
const UserCard({
required this.name,
required this.avatar,
required this.lastMessage,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Avatar(url: avatar),
title: Text(name, style: AppTypography.bodyLarge),
subtitle: Text(lastMessage, style: AppTypography.bodyMedium),
trailing: AppButton.secondary(
text: '发消息',
onPressed: onTap,
),
),
);
}
}
L3: Business Components(业务组件)
包含业务逻辑的组件,通常与 Feature 相关。
/// MessageBubble - 消息气泡(业务组件)
class MessageBubble extends ConsumerWidget {
final Message message;
const MessageBubble({required this.message});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isSender = message.senderId == ref.watch(currentUserProvider).id;
return Align(
alignment: isSender ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: EdgeInsets.symmetric(
horizontal: AppTokens.spacing16,
vertical: AppTokens.spacing8,
),
padding: EdgeInsets.all(AppTokens.spacing12),
decoration: BoxDecoration(
color: isSender ? AppColors.primary : AppColors.gray200,
borderRadius: BorderRadius.circular(AppTokens.radiusLarge),
),
child: Text(
message.content,
style: AppTypography.bodyMedium.copyWith(
color: isSender ? AppColors.white : AppColors.black,
),
),
),
);
}
}
L4: Pages(页面)
完整的页面,组合各种组件,连接 ViewModel。
/// ChatPage - 聊天页面
class ChatPage extends ConsumerWidget {
final String chatId;
const ChatPage({required this.chatId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(chatViewModelProvider(chatId));
return Scaffold(
appBar: AppBar(title: Text(state.chatName)),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.messages.length,
itemBuilder: (context, index) {
return MessageBubble(message: state.messages[index]);
},
),
),
ChatInputBar(
onSend: (text) {
ref.read(chatViewModelProvider(chatId).notifier).sendMessage(text);
},
),
],
),
);
}
}
UI 层目录结构
lib/
├── core/ui/
│ ├── base/ # L1: 基础定义(已实现)
│ │ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
│ │ ├── font.dart # TextStyle 定义 + textTheme(brightness)
│ │ └── app_theme.dart # ThemeData 组装(Light / Dark)
│ │
│ ├── components/ # L2: 原子组件
│ │ └── app_button.dart # 按钮(已实现)
│ │ # app_text_field / app_icon / app_avatar 等 待开发
│ │
│ └── composites/ # L3: 组合组件
│ └── app_dialog.dart # 确认弹窗(已实现)
│ # app_action_sheet / app_toast 等 待开发
│
└── features/
└── chat/
└── view/ # L4: 页面 + Feature 专属组件(待开发)
├── chat_page.dart
└── widgets/
├── message_bubble.dart
├── message_input_bar.dart
└── message_list_view.dart
核心价值
- 设计系统:确保设计与实现一致,与 Figma 完全对应
- 原子设计:从小到大构建组件,提升复用性
- 清晰分层:基础组件 vs 业务组件,职责明确
- 主题支持:亮色/暗色模式统一管理
- 易于维护:设计变更只需修改 Design System
3.2 多平台适配
核心理念:一套代码适配多个平台(iOS、Android、Web、Windows、macOS、Linux),通过平台检测和自适应组件实现平台特定的 UI 和交互。
支持的平台
| 平台类型 | 具体平台 | 设计规范 | 特点 |
|---|---|---|---|
| 移动端 | iOS、Android | Cupertino / Material Design | 触摸交互、竖屏优先 |
| 桌面端 | Windows、macOS、Linux | Fluent / macOS / GNOME | 鼠标键盘、大屏幕 |
| Web 端 | 浏览器 | 响应式设计 | 跨浏览器兼容 |
3.2.1 平台检测与适配策略
/// 平台工具类 - 统一平台检测
class PlatformAdapter {
// 平台类型判断
static bool get isMobile => Platform.isIOS || Platform.isAndroid;
static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
static bool get isIOS => Platform.isIOS;
static bool get isAndroid => Platform.isAndroid;
static bool get isWeb => kIsWeb;
static bool get isMacOS => Platform.isMacOS;
static bool get isWindows => Platform.isWindows;
// 设备类型判断(基于屏幕尺寸)
static DeviceType getDeviceType(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 600) return DeviceType.mobile;
if (width < 1200) return DeviceType.tablet;
return DeviceType.desktop;
}
// 获取平台特定的设计风格
static DesignStyle get designStyle {
if (isIOS) return DesignStyle.cupertino;
if (isAndroid) return DesignStyle.material;
if (isMacOS) return DesignStyle.macos;
if (isWindows) return DesignStyle.fluent;
return DesignStyle.material; // 默认
}
}
enum DeviceType { mobile, tablet, desktop }
enum DesignStyle { material, cupertino, fluent, macos }
3.2.2 响应式布局
根据屏幕尺寸自动调整布局:
/// 响应式布局组件
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
const ResponsiveLayout({
required this.mobile,
this.tablet,
this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 桌面布局(>= 1200px)
if (constraints.maxWidth >= 1200) {
return desktop ?? tablet ?? mobile;
}
// 平板布局(>= 600px)
else if (constraints.maxWidth >= 600) {
return tablet ?? mobile;
}
// 手机布局(< 600px)
else {
return mobile;
}
},
);
}
}
/// 使用示例
class ChatPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
// 手机:单栏布局
mobile: SingleColumnChatView(),
// 平板:两栏布局(会话列表 + 聊天)
tablet: TwoColumnChatView(),
// 桌面:三栏布局(联系人 + 会话列表 + 聊天)
desktop: ThreeColumnChatView(),
);
}
}
3.2.3 平台自适应组件
根据平台自动选择 Material 或 Cupertino 风格:
/// 平台自适应按钮
class PlatformButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
const PlatformButton({
required this.text,
this.onPressed,
});
@override
Widget build(BuildContext context) {
// iOS 使用 Cupertino 风格
if (PlatformAdapter.isIOS) {
return CupertinoButton(
onPressed: onPressed,
color: AppColors.primary,
child: Text(text),
);
}
// Android 和其他平台使用 Material 风格
else {
return ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
}
}
/// 平台自适应导航栏
class PlatformAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
const PlatformAppBar({
required this.title,
this.actions,
});
@override
Widget build(BuildContext context) {
// iOS 使用 CupertinoNavigationBar
if (PlatformAdapter.isIOS) {
return CupertinoNavigationBar(
middle: Text(title),
trailing: actions != null ? Row(children: actions!) : null,
);
}
// Android 使用 Material AppBar
else {
return AppBar(
title: Text(title),
actions: actions,
);
}
}
@override
Size get preferredSize => Size.fromHeight(56);
}
/// 平台自适应对话框
class PlatformDialog {
static Future<bool?> showConfirm(
BuildContext context, {
required String title,
required String content,
}) {
// iOS 使用 CupertinoAlertDialog
if (PlatformAdapter.isIOS) {
return showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: Text(content),
actions: [
CupertinoDialogAction(
child: Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
CupertinoDialogAction(
child: Text('确定'),
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
// Android 使用 Material AlertDialog
else {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
child: Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text('确定'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
}
}
3.2.4 平台特定交互
/// 平台特定的滑动返回手势
class PlatformScaffold extends StatelessWidget {
final Widget body;
final PreferredSizeWidget? appBar;
const PlatformScaffold({
required this.body,
this.appBar,
});
@override
Widget build(BuildContext context) {
final scaffold = Scaffold(
appBar: appBar,
body: body,
);
// iOS 支持侧滑返回
if (PlatformAdapter.isIOS) {
return CupertinoPageScaffold(
navigationBar: appBar as ObstructingPreferredSizeWidget?,
child: body,
);
}
return scaffold;
}
}
/// 平台特定的右键菜单(桌面端)
class PlatformContextMenu extends StatelessWidget {
final Widget child;
final List<ContextMenuItem> menuItems;
const PlatformContextMenu({
required this.child,
required this.menuItems,
});
@override
Widget build(BuildContext context) {
// 桌面端支持右键菜单
if (PlatformAdapter.isDesktop) {
return GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition);
},
child: child,
);
}
// 移动端使用长按显示菜单
return GestureDetector(
onLongPress: () {
_showMobileMenu(context);
},
child: child,
);
}
void _showContextMenu(BuildContext context, Offset position) {
// 显示桌面端右键菜单
}
void _showMobileMenu(BuildContext context) {
// 显示移动端底部菜单
}
}
3.2.5 屏幕尺寸断点
/// 屏幕断点定义
class ScreenBreakpoints {
// 手机
static const double mobile = 0;
static const double mobileMax = 599;
// 平板
static const double tablet = 600;
static const double tabletMax = 1199;
// 桌面
static const double desktop = 1200;
static const double desktopMax = 1919;
// 大屏
static const double ultraWide = 1920;
// 判断当前断点
static ScreenSize getSize(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < tablet) return ScreenSize.mobile;
if (width < desktop) return ScreenSize.tablet;
if (width < ultraWide) return ScreenSize.desktop;
return ScreenSize.ultraWide;
}
}
enum ScreenSize { mobile, tablet, desktop, ultraWide }
/// 响应式间距
class ResponsiveSpacing {
static double get(BuildContext context, {
double mobile = 16,
double tablet = 24,
double desktop = 32,
}) {
final size = ScreenBreakpoints.getSize(context);
switch (size) {
case ScreenSize.mobile:
return mobile;
case ScreenSize.tablet:
return tablet;
case ScreenSize.desktop:
case ScreenSize.ultraWide:
return desktop;
}
}
}
3.2.6 多平台目录结构
lib/
├── core/ui/
│ ├── base/ # 基础定义(已实现:colors / font / app_theme)
│ │ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
│ │ ├── font.dart # TextStyle 定义 + textTheme(brightness)
│ │ └── app_theme.dart # ThemeData 组装(可按平台扩展 Material / Cupertino)
│ │
│ ├── components/ # 基础组件
│ │ ├── app_button.dart
│ │ ├── app_text_field.dart
│ │ └── app_avatar.dart
│ │
│ ├── composites/ # 业务组合组件
│ │ ├── app_alert_dialog.dart
│ │ └── app_action_sheet.dart
│ │
│ └── platform/ # 平台适配(可选)
│ ├── platform_adapter.dart # 平台检测
│ ├── responsive_layout.dart # 响应式布局
│ └── screen_breakpoints.dart # 屏幕断点
│
└── features/
└── chat/
└── view/
├── chat_page.dart # 主入口(平台自适应)
└── layouts/ # 不同布局
├── mobile_layout.dart # 手机布局
├── tablet_layout.dart # 平板布局
└── desktop_layout.dart # 桌面布局
多平台适配核心价值
- 一套代码:维护成本低,所有平台同步更新
- 平台原生感:自动适配平台特定的设计规范和交互
- 响应式设计:自动适应不同屏幕尺寸
- 性能优化:根据平台特性优化渲染和交互
- 用户体验一致:核心功能在所有平台保持一致
3.2.7 平台适配最佳实践
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 导航栏 | PlatformAppBar | iOS 用 CupertinoNavigationBar,Android 用 AppBar |
| 对话框 | PlatformDialog | iOS 用 CupertinoAlertDialog,Android 用 AlertDialog |
| 按钮 | PlatformButton | iOS 用 CupertinoButton,Android 用 ElevatedButton |
| 滑动返回 | 自动检测平台 | iOS 支持侧滑返回,Android 使用返回按钮 |
| 右键菜单 | 桌面端显示,移动端长按 | 根据平台调整交互方式 |
| 布局 | ResponsiveLayout | 手机单栏、平板双栏、桌面三栏 |
3.3 Feature UI 组织
核心理念:UI 层按 Feature 组织,每个页面的 UI 组件都在其对应的 Feature 目录下。
chat_page.dart] Chat --> ChatWidgets[features/chat/view/widgets/
message_bubble.dart
message_input_bar.dart] ChatList --> ChatListPage[features/chat_list/view/
chat_list_page.dart] ChatList --> ChatListWidgets[features/chat_list/view/widgets/
chat_list_item.dart] Contact --> ContactPage[features/contact/view/
contact_page.dart] Contact --> ContactWidgets[features/contact/view/widgets/
contact_item.dart] Search --> SearchPage[features/search/view/
search_page.dart] Call --> CallPage[features/call/view/
call_page.dart] style UI fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style Chat fill:#fff9c4,stroke:#f57f17 style ChatList fill:#f3e5f5,stroke:#7b1fa2 style Contact fill:#e8f5e9,stroke:#388e3c style Search fill:#fce4ec,stroke:#c2185b style Call fill:#fff4e6,stroke:#f57c00
3.4 UI 层目录结构
lib/features/
├── chat/
│ └── view/
│ ├── chat_page.dart # 聊天页面
│ └── widgets/ # 聊天专用组件
│ ├── message_bubble.dart # 消息气泡
│ ├── message_input_bar.dart # 消息输入栏
│ └── message_list_view.dart # 消息列表
│
├── chat_list/
│ └── view/
│ ├── chat_list_page.dart # 会话列表页面
│ └── widgets/
│ ├── chat_list_item.dart # 会话列表项
│ ├── unread_badge.dart # 未读角标
│ └── pinned_indicator.dart # 置顶标识
│
├── contact/
│ └── view/
│ ├── contact_page.dart # 联系人页面
│ └── widgets/
│ ├── contact_item.dart # 联系人项
│ └── section_header.dart # 分组头
│
├── search/
│ └── view/
│ ├── search_page.dart # 搜索页面
│ └── widgets/
│ └── search_result_item.dart # 搜索结果项
│
└── call/
└── view/
├── call_page.dart # 通话页面
└── widgets/
└── call_controls.dart # 通话控制按钮
3.4 主要 Feature 页面
Chat Feature - 聊天功能
- 位置:
features/chat/view/chat_page.dart - 职责:消息列表展示、消息发送、多媒体消息、消息状态显示
- 专用组件:MessageBubble、InputBar、MessageList
Chat List Feature - 会话列表功能
- 位置:
features/chat_list/view/chat_list_page.dart - 职责:显示所有会话、未读消息提示、会话操作(删除/置顶)
- 专用组件:ChatItem、SwipeActions
Contact Feature - 联系人功能
- 位置:
features/contact/view/contact_page.dart - 职责:联系人列表、分组展示、联系人搜索、联系人详情
- 专用组件:ContactItem、SectionHeader
Search Feature - 搜索功能
- 位置:
features/search/view/search_page.dart - 职责:全局搜索、消息搜索、联系人搜索、搜索历史
- 专用组件:SearchResultItem
Call Feature - 通话功能
- 位置:
features/call/view/call_page.dart - 职责:语音通话、视频通话、通话控制、通话状态
- 专用组件:CallControls
设计原则:UI 层的每个页面都在其对应的 Feature 目录下,与该 Feature 的 Presentation 层和 Domain 层垂直对齐,形成高内聚的功能模块。
路由系统(go_router)
路由是什么
路由就是「页面地址 → 页面」的映射表。打开 App 时系统根据当前地址决定显示哪个页面,点击按钮时通过地址跳转到另一个页面。
Shell 是什么
Shell(壳层)是一个持久存在的 UI 框架,内容区域在里面切换,而框架本身不销毁。类比一下:
- 微信底部有「微信 / 通讯录 / 发现 / 我」四个 Tab,这四个 Tab 的底部导航栏始终可见,这就是 Shell
- 点击不同 Tab,底部导航栏不动,只有上方内容在切换
- 但打开「设置 → 关于微信」这类页面时,底部导航栏消失了,这是跳出了 Shell
在本项目里:
- Shell 内(带导航栏):
/chat、/contact、/settings——底部导航栏始终可见 - Shell 外(全屏):
/chat/detail、/chat/:id、/settings/theme、/login——全屏独占,没有底部导航栏
AppTab 就是 Shell 组件,它只负责渲染底部导航栏和容纳当前页面内容,自身不包含任何业务逻辑。
为什么禁止使用 Navigator.push
传统写法:
// 禁止 ❌
Navigator.push(context, MaterialPageRoute(builder: (_) => const ThemeView()));
禁止原因:
- 绕过了守卫:直接
Navigator.push不经过 go_router 的redirect,未登录用户可以直接跳进受保护页面 - 路径分散:目标页面的引用散落在各处
onTap,重构时要全局搜索替换 - 破坏 Shell:在 go_router 管理的路由中混用
Navigator.push,可能导致底部导航栏消失或 Tab 状态丢失 - 深链接失效:go_router 无法感知通过
Navigator.push打开的页面,通知点击跳转等场景会出问题
正确写法:
// 正确 ✅
context.push(AppRouteName.settingsTheme.path); // 压栈,可以返回
context.go(AppRouteName.chat.path); // 替换历史,不可返回
go_router 集中解决上述问题:统一声明路由、统一拦截、路径字符串集中在 AppRouteName 枚举管理。
文件结构
app/router/
├── app_router.dart # 路由表 + routerProvider(核心)
├── app_route_name.dart # AppRouteName 枚举,路径常量 + fromPath()
└── guards/
└── auth_guard.dart # 登录守卫(拦截未登录访问)
路径常量:app_route_name.dart
所有路径字符串只在这一个文件里写,用枚举定义。其他地方引用 AppRouteName.xxx.path,不允许硬编码字符串。
用枚举而不是常量类,是因为守卫里的 switch 需要穷举枚举值:新加路由时,若守卫的 switch 没有补对应的 case,编译器直接报错,防止漏掉权限判断。
enum AppRouteName {
// ── Shell 内(Tab 根路由)────────────────────────────────────────
chat('/chat'),
contact('/contact'),
settings('/settings'),
// ── Shell 外(全屏页面,无底部导航栏)──────────────────────────────
// extra: ({String conversationId, String title})
chatDetail('/chat/detail'),
// 路径参数形式:导航用 AppRouteName.chatDetailByIdPath(id),不直接用 .path
chatDetailById('/chat/:id'),
settingsTheme('/settings/theme'),
login('/login');
const AppRouteName(this.path);
/// 绝对路径,用于 context.push / context.go 导航及顶层路由表声明
final String path;
/// 从绝对路径反查枚举值,路径未注册时返回 null
/// 注意:含路径参数的路由(如 /chat/99)无法匹配,返回 null,
/// auth_guard 会按受保护路由处理
static AppRouteName? fromPath(String path) =>
AppRouteName.values.where((r) => r.path == path).firstOrNull;
/// 生成 chatDetailById 的实际导航路径,将 :id 替换为真实 id
/// 例:AppRouteName.chatDetailByIdPath('99') → '/chat/99'
static String chatDetailByIdPath(String id) => '/chat/$id';
}
规则:任何地方都不允许硬编码路径字符串。
context.push / context.go用.path;含路径参数的路由用对应的静态方法(如chatDetailByIdPath);路径字符串只在枚举里写一次。
路由表结构:app_router.dart
整个路由表分两类:
| 类型 | 路由 | TabBar |
|---|---|---|
| Shell 内(StatefulShellRoute branches) | /chat、/contact、/settings | 始终可见 |
| Shell 外(parentNavigatorKey = _rootKey) | /chat/detail、/chat/:id、/settings/theme、/login | 隐藏 |
// Root Navigator Key:全屏路由声明 parentNavigatorKey 时引用,
// 确保 push 时覆盖整个 Shell(TabBar 消失)
final _rootKey = GlobalKey<NavigatorState>();
final routerProvider = Provider<GoRouter>((ref) {
final authNotifier = ref.read(authNotifierProvider);
return GoRouter(
// Root Navigator 的 Key,供全屏路由声明 parentNavigatorKey 使用
navigatorKey: _rootKey,
// 冷启动默认落地页;authGuard 会在进入前检查登录状态并按需重定向
initialLocation: AppRouteName.chat.path,
// 在控制台打印每次路由变化,方便开发期间调试;上线前设为 false
debugLogDiagnostics: true,
// 监听 authNotifier:登录 / 退出时自动触发 redirect 重新执行
refreshListenable: authNotifier,
redirect: (context, state) => authGuard(authNotifier, state),
routes: [
// ── Shell 内:底部导航栏始终可见 ─────────────────────────────────
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return AppTab(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.chat.path, builder: (_, __) => const ChatPage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.contact.path, builder: (_, __) => const ContactPage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.settings.path, builder: (_, __) => const SettingsPage()),
]),
],
),
// ── Shell 外:全屏页面,无底部导航栏 ─────────────────────────────
// parentNavigatorKey: _rootKey 确保路由覆盖 Shell,TabBar 消失
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetail.path,
builder: (context, state) {
final extra = state.extra as ({String conversationId, String title});
return ChatDetailPage(conversationId: extra.conversationId, title: extra.title);
},
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetailById.path,
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailPage(conversationId: id, title: '路径参数详情');
},
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.settingsTheme.path,
builder: (_, __) => const ThemeView(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.login.path,
builder: (_, __) => const LoginPage(),
),
],
);
});
StatefulShellRoute 是什么
StatefulShellRoute.indexedStack 负责维护底部 Tab 导航,每个 branches 对应一个 Tab 分支。它和普通 IndexedStack 的区别:
| IndexedStack(旧方式) | StatefulShellRoute(go_router) | |
|---|---|---|
| Tab 状态保持 | ✅ 是 | ✅ 是 |
| 路径支持 | ❌ 无路径概念 | ✅ 每个 Tab 都有真实 URL 路径 |
| 深链接 | ❌ 不支持 | ✅ 支持 |
| Tab 内子页面 | ❌ 需手动处理 | ✅ 独立 Navigator 栈 |
| 守卫 | ❌ 无统一拦截 | ✅ 全局 redirect |
branches 是并列关系,不是层级关系。三个 Tab 分支互相独立,各自维护自己的导航栈:
StatefulShellRoute(Shell 内,TabBar 可见)
branches[0] → /chat ← 聊天 Tab(独立栈)
branches[1] → /contact ← 联系人 Tab(独立栈)
branches[2] → /settings ← 设置 Tab(独立栈)
Root Navigator(Shell 外,TabBar 隐藏,parentNavigatorKey: _rootKey)
/chat/detail ← extra 传参详情页
/chat/:id ← 路径参数详情页
/settings/theme ← 主题设置页
/login ← 登录页
如何在页面间跳转
用 context 调用,不需要 ref:
| 场景 | 方法 | 说明 |
|---|---|---|
| 进入子页面(可返回,TabBar 隐藏) | context.push(AppRouteName.xxx.path) | 压到 Root Navigator,覆盖 Shell,有返回按钮 |
| 带参数进入子页面(extra) | context.push(AppRouteName.xxx.path, extra: obj) | extra 传 Dart 对象;路由 builder 解包后以构造参数注入目标页 |
| 带参数进入子页面(路径参数) | context.push(AppRouteName.xxxByIdPath(id)) | id 内嵌在 URL 中;适合深链接 / 推送通知跳转 |
| 切换 Tab(TabBar 可见) | context.go(AppRouteName.xxx.path) | 替换整个历史,Tab 高亮切换,不可返回 |
| 登录成功跳首页 | context.go(AppRouteName.chat.path) | 替换历史,防止返回到登录页 |
| 返回上一页 | context.pop() | 弹栈 |
| 返回并传值给上层 | context.pop(result) | 上层用 await context.push(...) 接收 |
| 弹窗 / Alert | showDialog(...) | Flutter 原生,go_router 不管理,直接用 |
| 底部弹层 | showModalBottomSheet(...) | Flutter 原生,go_router 不管理,直接用 |
// 进入子页面(TabBar 隐藏,可返回)
context.push(AppRouteName.settingsTheme.path);
// 带参数进入子页面:extra 传 Dart Record
context.push(
AppRouteName.chatDetail.path,
extra: (conversationId: '42', title: '技术支持'),
);
// 带参数进入子页面:路径参数
context.push(AppRouteName.chatDetailByIdPath('99'));
// 切换 Tab(TabBar 可见,不可返回)
context.go(AppRouteName.contact.path);
// 返回
context.pop();
// 返回并传值
final result = await context.push<String>(AppRouteName.settingsTheme.path);
// 弹窗(Flutter 原生)
showDialog(context: context, builder: (_) => const AlertDialog(...));
禁止使用
Navigator.push(context, MaterialPageRoute(...)):不经过 go_router redirect,守卫失效;路径散落各处;破坏 Tab 状态;深链接 / 通知跳转失效。
带参数路由
go_router 有两种传参方式,根据场景选择:
| extra(传对象) | 路径参数(:id) | |
|---|---|---|
| 适用场景 | 列表点入详情,导航时已有完整数据 | 推送通知深链接,只携带一个 ID |
| URL 可见 | 否 | 是 |
| 数据来源 | 直接传对象,不需要额外请求 | 按 ID 从 Repository 重新拉取 |
| 代码量 | 少 | 多(需 .family provider) |
日常开发优先用 extra;接入推送通知后,需要从通知冷启动进入会话时,再补路径参数版本。
extra 传参完整示例
Step 1:枚举声明路由(静态路径,注释写明 extra 类型)
// app_route_name.dart
// Chat 子路由
// extra: ({String conversationId, String title})
chatDetail('/chat/detail'),
Step 2:路由表注册,builder 负责解包 extra
// app_router.dart
// Shell 外顶层路由:parentNavigatorKey: _rootKey 覆盖整个 Shell,TabBar 隐藏
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.chatDetail.path, // '/chat/detail'
builder: (context, state) {
// 路由层唯一做类型转换的地方,目标页面只知道构造参数
final extra = state.extra as ({String conversationId, String title});
return ChatDetailPage(
conversationId: extra.conversationId,
title: extra.title,
);
},
),
Step 3:目标页面只接受构造参数,不感知 GoRouter
// chat_detail_page.dart
class ChatDetailPage extends StatelessWidget {
const ChatDetailPage({
super.key,
required this.conversationId,
required this.title,
});
final String conversationId;
final String title;
// ...
}
Step 4:导航时附带 extra
context.push(
AppRouteName.chatDetail.path,
extra: (conversationId: conversation.id, title: conversation.title),
);
extra 使用 Dart 3 匿名 Record,轻量且类型安全,无需定义额外 class。
AppTab:Tab 如何切换
AppTab 是底部导航栏的持久容器,负责渲染底部 Tab 栏和持有当前 Tab 的内容区域。它自身不管理任何状态,Tab 的当前索引由 go_router 传入的 StatefulNavigationShell 维护:
class AppTab extends StatelessWidget {
const AppTab({super.key, required this.navigationShell});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell, // 当前 Tab 的 Navigator 内容
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex, // go_router 维护
onTap: (index) => navigationShell.goBranch(
index,
// 再次点击已激活的 Tab 时回到该 Tab 的首页
initialLocation: index == navigationShell.currentIndex,
),
items: const [...],
),
);
}
}
上面的 goBranch 只用在 AppTab 内部的 onTap,处理「重复点同一 Tab 时回到该 Tab 首页」的导航栏专属逻辑。业务页面里不调 goBranch。
从业务页面切换 Tab
在任意页面主动切换 Tab,用 context.go:
// 切换到联系人 Tab
context.go(AppRouteName.contact.path);
// 切换到设置 Tab
context.go(AppRouteName.settings.path);
| context.go | context.push | |
|---|---|---|
| 历史栈 | 替换(不可返回) | 压栈(可返回) |
| 返回按钮 | 不显示 | 自动显示 |
| 适用场景 | 切换 Tab、登录后跳首页 | 进入子页面 |
Shell 外路由的路径前缀不影响 TabBar
Shell 外路由(声明了 parentNavigatorKey: _rootKey)放到 Root Navigator 上,会覆盖整个 Shell,TabBar 始终隐藏,与路径前缀无关。
例如 /settings/theme 虽然前缀是 /settings,但它是 Shell 外路由,导航后 TabBar 不可见:
// 全屏打开主题页,TabBar 隐藏,可返回
context.push(AppRouteName.settingsTheme.path);
如果需要"在某个 Tab 内打开子页面、保留 TabBar",要把子路由注册为 Shell 内路由(放进对应 StatefulShellBranch 的 routes,不加 parentNavigatorKey)。
登录守卫:auth_guard.dart
守卫是 go_router 的全局拦截器,每次跳转路由前都会执行。返回 null 表示放行,返回路径字符串表示重定向到那个路径。
String? authGuard(AuthNotifier authNotifier, GoRouterState state) {
final isLoggedIn = authNotifier.isLoggedIn;
final route = AppRouteName.fromPath(state.matchedLocation);
// 路径不在枚举中(理论上不应出现)→ 按受保护处理
if (route == null) return isLoggedIn ? null : AppRouteName.login.path;
switch (route) {
case AppRouteName.login:
// 已登录还在登录页 → 跳聊天页
return isLoggedIn ? AppRouteName.chat.path : null;
case AppRouteName.chat:
case AppRouteName.chatDetail:
case AppRouteName.chatDetailById:
case AppRouteName.contact:
case AppRouteName.settings:
case AppRouteName.settingsTheme:
// 受保护路由 → 未登录跳登录页
return isLoggedIn ? null : AppRouteName.login.path;
}
}
用 switch(route) 而不是 if-else 的原因:Dart 的枚举 switch 是穷举的。在 AppRouteName 里新增一个枚举值(即新加路由),如果忘了在守卫的 switch 里补对应的 case,编译器会直接报错,强制你决定这条路由的权限。
refreshListenable 机制
守卫只在「路由跳转」时执行一次。但当用户「退出登录」后,页面没有跳转,守卫不会自动重新执行。
refreshListenable 解决这个问题:把 AuthNotifier(它继承自 ChangeNotifier)传给 go_router,每当 AuthNotifier.notifyListeners() 被调用时,go_router 自动重新执行 redirect:
GoRouter(
refreshListenable: authNotifier, // 监听 AuthNotifier
redirect: (context, state) => authGuard(authNotifier, state),
...
)
// 当用户点击「退出登录」:
ref.read(authNotifierProvider).logout();
// ↓ AuthNotifier.logout() 调用 notifyListeners()
// ↓ go_router 收到通知,重新执行 authGuard
// ↓ authGuard 发现未登录,返回 AppRouteName.login.path
// ↓ 自动跳转到登录页
如何添加一个新路由
以添加「个人资料页 /profile」为例,共三步:
Step 1:在 AppRouteName 枚举追加新值
// app_route_name.dart
enum AppRouteName {
chat('/chat'),
contact('/contact'),
settings('/settings'),
settingsTheme('/settings/theme'),
login('/login'),
profile('/profile'); // 新增
...
}
加完之后,守卫的 switch 会立即编译报错,提示你补上 case AppRouteName.profile:,决定这条路由是否需要登录。
Step 2:在守卫 switch 补 case
// auth_guard.dart
switch (route) {
case AppRouteName.login:
return isLoggedIn ? AppRouteName.chat.path : null;
case AppRouteName.chat:
case AppRouteName.chatDetail:
case AppRouteName.chatDetailById:
case AppRouteName.contact:
case AppRouteName.settings:
case AppRouteName.settingsTheme:
case AppRouteName.profile: // 新增,受保护
return isLoggedIn ? null : AppRouteName.login.path;
}
Step 3:在 app_router.dart 注册路由
全屏页面(无底部导航栏):加到顶层 routes 列表,加上 parentNavigatorKey: _rootKey,确保覆盖整个 Shell、TabBar 隐藏。
// app_router.dart(Shell 外,全屏)
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.profile.path,
builder: (_, __) => const ProfilePage(),
),
Tab 内子页面(保留 TabBar):加到对应 StatefulShellBranch 的 routes 里,不加 parentNavigatorKey,路由放到 Branch Navigator,TabBar 保持可见。
// app_router.dart(Shell 内,TabBar 可见)
StatefulShellBranch(
routes: [
GoRoute(
path: AppRouteName.settings.path,
builder: (_, __) => const SettingsPage(),
routes: [
// 此处不加 parentNavigatorKey,路由在 Branch Navigator 内
GoRoute(path: AppRouteName.profile.segment, builder: (_, __) => const ProfilePage()),
],
),
],
),
Step 4:在需要的地方跳转
onTap: () => context.push(AppRouteName.profile.path),
接入正式 token(storage_sdk 就绪后)
当前守卫用 AuthNotifier._isLoggedIn(内存变量,重启后重置)做 Demo。storage_sdk 接入后只需修改 AuthNotifier,守卫本身无需改动:
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
// 改为从安全存储读取:
Future<void> initialize() async {
final token = await secureStorage.read('token');
_isLoggedIn = token != null && token.isNotEmpty;
notifyListeners();
}
Future<void> login(String token) async {
await secureStorage.write('token', token);
_isLoggedIn = true;
notifyListeners();
}
Future<void> logout() async {
await secureStorage.delete('token');
_isLoggedIn = false;
notifyListeners();
}
}
Presentation 层模块详解
4.1 Presentation 层职责
Presentation 层实现 MVVM 模式中的 ViewModel,负责:
- 管理 UI 状态
- 处理用户交互逻辑
- 调用 Repository(复杂多步编排场景可提取 UseCase)
- 数据格式转换(Entity → UI Model)
- 通知 UI 更新
4.2 Feature Presentation 组织
核心理念:每个 Feature 的 ViewModel 都在其对应的 Feature 目录下的 presentation/ 子目录中。
chat_view_model.dart + chat_state.dart] ChatListPres --> ChatListVM[features/chat_list/presentation/
chat_list_view_model.dart] ContactPres --> ContactVM[features/contact/presentation/
contact_view_model.dart] SearchPres --> SearchVM[features/search/presentation/
search_view_model.dart] CallPres --> CallVM[features/call/presentation/
call_view_model.dart] style Presentation fill:#fff4e6,stroke:#f57c00,stroke-width:3px style ChatPres fill:#fff9c4,stroke:#f57f17 style ChatListPres fill:#f3e5f5,stroke:#7b1fa2 style ContactPres fill:#e8f5e9,stroke:#388e3c style SearchPres fill:#fce4ec,stroke:#c2185b style CallPres fill:#e1f5ff,stroke:#0288d1
4.3 Presentation 层目录结构
lib/features/
├── chat/
│ └── presentation/
│ ├── chat_view_model.dart # 聊天状态管理(直接方法调用,副作用用 ref.listen)
│ └── chat_state.dart # State(@freezed 不可变状态)
│
├── chat_list/
│ └── presentation/
│ ├── chat_list_view_model.dart # 会话列表状态管理
│ └── chat_list_state.dart # State
│
├── contact/
│ └── presentation/
│ └── contact_view_model.dart # 联系人状态管理(简单 feature 可 State 内联)
│
├── search/
│ └── presentation/
│ └── search_view_model.dart # 搜索状态管理(简单 feature 可 State 内联)
│
└── call/
└── presentation/
├── call_view_model.dart # 通话状态管理
└── call_state.dart # State
4.4 ViewModel 设计
Riverpod ViewModel 实现方式
使用 Riverpod,有两种主要的 ViewModel 实现方式:
方式一:标准方式(StateNotifier + 手动定义)
// 1. 定义 State 类(使用 freezed)
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List<Message> messages,
@Default(false) bool isLoading,
@Default('') String error,
Message? selectedMessage,
}) = _ChatState;
}
// 2. 定义 ViewModel — 直接调用 Repository
class ChatViewModel extends StateNotifier<ChatState> {
ChatViewModel(this._chatRepository) : super(const ChatState());
final ChatRepository _chatRepository;
// 发送消息
Future<void> sendMessage(String content) async {
state = state.copyWith(isLoading: true, error: '');
try {
await _chatRepository.sendMessage(content);
await loadMessages(); // 重新加载消息列表
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
// 加载消息
Future<void> loadMessages() async {
state = state.copyWith(isLoading: true);
try {
final messages = await _chatRepository.getMessages();
state = state.copyWith(messages: messages);
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
// 选择消息
void selectMessage(Message message) {
state = state.copyWith(selectedMessage: message);
}
}
// 3. 定义 Provider
final chatViewModelProvider =
StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
return ChatViewModel(ref.watch(chatRepositoryProvider));
});
方式二:现代方式(Notifier + 代码生成)⭐ 推荐
// 使用 riverpod_generator 和 freezed
part 'chat_view_model.g.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List<Message> messages,
@Default(false) bool isLoading,
@Default('') String error,
}) = _ChatState;
}
@riverpod
class ChatViewModel extends _$ChatViewModel {
@override
ChatState build() => const ChatState();
// ViewModel 直接调用 Repository
Future<void> sendMessage(String content) async {
state = state.copyWith(isLoading: true);
try {
await ref.read(chatRepositoryProvider).sendMessage(content);
await loadMessages();
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
Future<void> loadMessages() async {
state = state.copyWith(isLoading: true);
try {
final messages = await ref.read(chatRepositoryProvider).getMessages();
state = state.copyWith(messages: messages);
} finally {
state = state.copyWith(isLoading: false);
}
}
}
UI 层使用 ViewModel
class ChatPage extends ConsumerWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 监听状态变化 → 自动重建 UI
final state = ref.watch(chatViewModelProvider);
final viewModel = ref.read(chatViewModelProvider.notifier);
// ─── 副作用处理(替代 Effect 文件) ───
// ref.listen() 在状态变化时触发,不会重建 Widget
ref.listen(chatViewModelProvider.select((s) => s.error), (prev, next) {
if (next.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next)),
);
}
});
return Scaffold(
appBar: AppBar(title: const Text('聊天')),
body: Column(
children: [
// 消息列表
Expanded(
child: state.isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: state.messages.length,
itemBuilder: (context, index) {
final message = state.messages[index];
return MessageBubble(message: message);
},
),
),
// 输入框
ChatInputArea(
onSend: (content) => viewModel.sendMessage(content),
),
],
),
);
}
}
为什么不需要 Event / Effect 文件?
在 BLoC 模式中,Event 用于触发状态变更,Effect 用于处理副作用(导航、Toast)。
Riverpod 中这两个概念有更自然的替代方案:
• Event → 直接方法调用:
viewModel.sendMessage(content),无需中间层• Effect →
ref.listen():监听状态字段变化,在 View 层触发导航/弹窗
4.5 主要 ViewModel
ChatViewModel
- 位置:
features/chat/presentation/chat_view_model.dart - 职责:消息列表状态管理、发送消息逻辑、消息加载分页、消息状态更新
- 调用:ChatRepository(直接调用)
ChatListViewModel
- 位置:
features/chat_list/presentation/chat_list_view_model.dart - 职责:会话列表状态、未读数管理、会话操作
- 调用:ChatRepository(直接调用)
ContactViewModel
- 位置:
features/contact/presentation/contact_view_model.dart - 职责:联系人列表状态、联系人搜索、联系人操作
- 调用:ContactRepository(直接调用)
SearchViewModel
- 位置:
features/search/presentation/search_view_model.dart - 职责:全局搜索状态管理、搜索历史管理
- 调用:多个 Repository(跨模块搜索)
CallViewModel
- 位置:
features/call/presentation/call_view_model.dart - 职责:通话状态管理、通话控制逻辑
- 调用:CallRepository(直接调用)
设计原则:每个 Feature 的 ViewModel 独立管理该 Feature 的状态,直接调用 Repository 执行数据操作。当业务逻辑复杂(多步编排、跨模块协调)时,可提取 UseCase 封装。
Domain 层模块详解
5.1 Domain 层职责
Domain 层是整洁架构的核心,分为两部分:
Feature 专属 Domain(features/*/domain/)
- Use Cases:封装该 Feature 的业务逻辑
- Entities:该 Feature 特有的Domain 实体
全局共享 Domain(domain/)
- Repository 接口:定义数据访问接口
- Value Objects:跨 Feature 的值对象
5.2 Domain 层架构
Repository 接口定义] ValueObjects[domain/value_objects/
共享值对象] end ChatDomain --> |使用| RepoInterfaces ChatListDomain --> |使用| RepoInterfaces ContactDomain --> |使用| RepoInterfaces style FeatureDomain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style GlobalDomain fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
5.3 Feature Domain 目录结构
lib/features/
├── chat/
│ └── domain/
│ ├── usecases/
│ │ ├── send_message_usecase.dart # 发送消息
│ │ ├── load_messages_usecase.dart # 加载消息
│ │ └── delete_message_usecase.dart # 删除消息
│ └── entities/
│ └── message.dart # 消息实体
│
├── chat_list/
│ └── domain/
│ └── usecases/
│ ├── load_chat_list_usecase.dart # 加载会话列表
│ ├── update_chat_usecase.dart # 更新会话
│ └── delete_chat_usecase.dart # 删除会话
│
├── contact/
│ └── domain/
│ ├── usecases/
│ │ ├── load_contacts_usecase.dart # 加载联系人
│ │ └── search_contact_usecase.dart # 搜索联系人
│ └── entities/
│ └── contact.dart # 联系人实体
│
├── search/
│ └── domain/
│ └── usecases/
│ └── search_usecase.dart # 全局搜索
│
└── call/
└── domain/
└── usecases/
├── initiate_call_usecase.dart # 发起通话
├── answer_call_usecase.dart # 接听通话
└── end_call_usecase.dart # 结束通话
5.4 全局 Domain 目录结构
lib/domain/
├── repositories/ # Repository 接口定义(依赖倒置)
│ ├── message_repository.dart # 消息仓库接口
│ ├── chat_repository.dart # 会话仓库接口
│ ├── contact_repository.dart # 联系人仓库接口
│ ├── user_repository.dart # 用户仓库接口
│ └── call_repository.dart # 通话仓库接口
│
└── value_objects/ # 值对象
├── user_id.dart # 用户 ID 值对象
├── message_id.dart # 消息 ID 值对象
└── chat_id.dart # 会话 ID 值对象
5.5 Use Case 设计(可选层)
⚠️ UseCase 是可选的
大多数 Feature 的 ViewModel 可以直接调用 Repository,无需 UseCase 中间层。
只在以下场景提取 UseCase:
• 多步业务编排:如登录后需写 Token + 更新用户信息 + 上报设备
• 跨模块协调:一个操作需要调用多个 Repository
• 复杂业务规则:格式校验、权限判断、重试策略等
• 多 ViewModel 复用:同一业务逻辑被多个页面调用
典型案例:
LoginUseCase(登录 = 调接口 + 写 Token + 转实体,属于多步编排)
当确实需要 UseCase 时,遵循单一职责原则:
domain/repositories/] VM -.->|复杂场景| UC[UseCase
features/*/usecases/] UC -->|通过接口| Repo Repo -.实现.-> RepoImpl[Repository Impl
data/repositories/] RepoImpl -->|返回| Entity[Entity] style VM fill:#fff4e6,stroke:#f57c00 style UC fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5 style Repo fill:#e8f5e9,stroke:#388e3c style RepoImpl fill:#e8f5e9,stroke:#388e3c style Entity fill:#f3e5f5,stroke:#7b1fa2
5.6 主要 Use Cases
Chat Feature Use Cases
- 位置:
features/chat/usecases/ SendMessageUseCase:发送消息LoadMessagesUseCase:加载消息DeleteMessageUseCase:删除消息
Chat List Feature Use Cases
- 位置:
features/chat_list/usecases/ LoadChatListUseCase:加载会话列表UpdateChatUseCase:更新会话DeleteChatUseCase:删除会话
Contact Feature Use Cases
- 位置:
features/contact/usecases/ LoadContactsUseCase:加载联系人SearchContactUseCase:搜索联系人
Call Feature Use Cases
- 位置:
features/call/usecases/ InitiateCallUseCase:发起通话AnswerCallUseCase:接听通话EndCallUseCase:结束通话
5.7 Repository 接口
位置:domain/repositories/(全局目录)
Domain 层只定义接口,不包含实现:
MessageRepository:消息数据访问接口ChatRepository:会话数据访问接口ContactRepository:联系人数据访问接口CallRepository:通话数据访问接口
关键原则:
1. UseCase 按需创建:只在多步编排、跨模块协调等复杂场景使用,简单 CRUD 直接调 Repository
2. UseCase 在 Feature 目录:需要时,放在 features/*/usecases/ 下
3. Repository 接口在全局 domain/:所有 Repository 接口定义在 domain/repositories/ 下
4. 依赖倒置:UseCase / ViewModel 依赖 Repository 接口,不依赖具体实现
Data 层模块详解
6.1 Data 层职责
Data 层是全局目录,负责:
- 实现 Domain 层(全局 domain/)定义的 Repository 接口
- 协调本地和远程数据源
- 数据缓存策略
- 数据格式转换(DTO ↔ Entity)
6.2 Data 层目录结构
lib/data/
├── repositories/ # Repository 实现
│ ├── message_repository_impl.dart # 实现 MessageRepository 接口
│ ├── chat_repository_impl.dart # 实现 ChatRepository 接口
│ ├── contact_repository_impl.dart # 实现 ContactRepository 接口
│ ├── user_repository_impl.dart # 实现 UserRepository 接口
│ └── call_repository_impl.dart # 实现 CallRepository 接口
│
├── local/ # 本地数据源
│ ├── message_local_ds.dart # 消息本地数据源
│ ├── chat_local_ds.dart # 会话本地数据源
│ ├── contact_local_ds.dart # 联系人本地数据源
│ ├── user_local_ds.dart # 用户本地数据源
│ │
│ ├── drift/ # Drift 数据库
│ │ ├── app_database.dart # Drift 数据库定义
│ │ ├── app_database.g.dart # Drift 生成代码
│ │ # database_connection.dart 已迁移至 storage_sdk(数据库连接与 Isolate 生命周期由 SDK 层统一管理)
│ │ ├── tables/ # 表定义
│ │ │ ├── message_table.dart # 消息表
│ │ │ ├── conversation_table.dart # 会话表
│ │ │ └── user_table.dart # 用户表
│ │ ├── daos/ # 数据访问对象
│ │ │ ├── message_dao.dart # 消息 DAO
│ │ │ ├── conversation_dao.dart # 会话 DAO
│ │ │ └── user_dao.dart # 用户 DAO
│ │ ├── migrations/ # 数据库迁移
│ │ │ ├── migration_v1.dart # V1 迁移脚本
│ │ │ └── migration_runner.dart # 迁移执行器
│ │ └── mappers/ # DB ↔ DTO 映射
│ │ ├── message_mapper.dart # 消息映射
│ │ └── conversation_mapper.dart # 会话映射
│ │
│ └── storage/ # 其他本地存储
│ ├── preference_storage.dart # SharedPreferences 封装
│ ├── file_storage.dart # 文件存储管理
│ └── image_cache.dart # 图片缓存
│
├── remote/ # Request 文件(一个端点一个文件,Repository 直接调 NetworksSdkApi)
│ ├── login_request.dart # 登录端点
│ ├── logout_request.dart # 登出端点
│ ├── send_message_request.dart # 发消息端点
│ └── ... # 其他端点 Request 文件
│
├── cache/ # 缓存
│ ├── cache_manager.dart # 缓存管理器
│ └── cache_policies.dart # 缓存策略
│
└── models/ # DTO(统一归口,local / remote 共用)
├── message_dto.dart # 消息 DTO
├── conversation_dto.dart # 会话 DTO
├── user_dto.dart # 用户 DTO
├── contact_dto.dart # 联系人 DTO
└── call_dto.dart # 通话 DTO
6.3 Repository 实现
domain/repositories/
Repository 接口] -.实现.-> Repo[Data Layer
data/repositories/
Repository 实现] Repo -->|读取| LocalDS[Local DataSource
data/local/] Repo -->|请求| SdkApi[NetworksSdkApi
networks_sdk] Repo -->|缓存| Cache[Cache Manager
data/cache/] LocalDS -->|Drift| DB[(Database)] SdkApi -->|HTTP/WebSocket| API[API Server] Cache -->|内存| Memory[Memory Cache] style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style LocalDS fill:#c8e6c9,stroke:#388e3c style SdkApi fill:#c8e6c9,stroke:#388e3c style Cache fill:#c8e6c9,stroke:#388e3c
6.4 Data Source 详解
Local DataSource(data/local/)
- Drift 数据库访问:使用 packages/storage_sdk 提供的 Drift 封装
- 本地文件存储:媒体文件、缓存文件
- Secure Storage:敏感数据(token、密钥)
// 示例:MessageLocalDataSource
class MessageLocalDataSource {
final DriftSDK _db;
Future<List<MessageDTO>> getMessages(String chatId) {
return _db.query('messages', where: 'chat_id = ?', whereArgs: [chatId]);
}
}
Request 文件(data/remote/)
- 一个端点 = 一个文件:Response DTO + Request 类放在同一文件中
- Repository 直接调 NetworksSdkApi:无需 RemoteDataSource 中间层
- @ApiRequest 注解 + 代码生成:自动实现 path / method / toJson / fromJson 注册(Request 无需 @JsonSerializable)
// 示例:Repository 直接调用 Request
// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
final NetworksSdkApi _client;
Future<SendMessageData?> sendMessage({
required String chatId,
required String content,
}) {
return _client.executeRequest(
SendMessageRequest(chatId: chatId, content: content),
);
}
}
Cache Manager(data/cache/)
- 内存缓存:频繁访问的数据
- 缓存策略:TTL、LRU 等策略
- 缓存失效:自动或手动失效
6.5 DTO Models(data/models/)
Data Transfer Objects 用于数据传输:
- 与 API 数据结构对应:JSON 序列化/反序列化
- 与 Entity 互相转换:DTO ↔ Entity 转换方法
- 包含数据库映射:Drift 表结构映射
// 示例:MessageDTO
class MessageDTO {
final String id;
final String content;
final String chatId;
final DateTime timestamp;
// 从 JSON 反序列化
factory MessageDTO.fromJson(Map<String, dynamic> json) { ... }
// 转换为 Entity
Message toEntity() {
return Message(
id: MessageId(id),
content: content,
chatId: ChatId(chatId),
timestamp: timestamp,
);
}
// 从 Entity 转换
factory MessageDTO.fromEntity(Message entity) { ... }
}
6.6 数据流转
domain/repositories/] RepoInterface -.实现.-> RepoImpl[Repository Impl
data/repositories/] RepoImpl -->|1. 检查缓存| Cache[Cache] RepoImpl -->|2. 读取本地| LocalDS[Local DS] RepoImpl -->|3. 请求远程| SdkApi2[NetworksSdkApi] SdkApi2 -->|DTO| RepoImpl RepoImpl -->|转换| Entity[Entity] Entity -->|返回| UC style RepoInterface fill:#f3e5f5,stroke:#7b1fa2 style RepoImpl fill:#e8f5e9,stroke:#388e3c style UC fill:#f3e5f5,stroke:#7b1fa2 style Entity fill:#f3e5f5,stroke:#7b1fa2
关键原则:
1. Data 层是全局目录:所有 Repository 实现都在 data/repositories/ 下
2. 实现全局接口:实现 domain/repositories/ 中定义的接口
3. 统一数据源管理:按类型组织(local/remote/cache),不按 Feature 划分
4. DTO 与 Entity 分离:DTO 在 Data 层,Entity 在 Domain 层
Core 层模块详解
7.1 Core 层职责
Core 层保留主 App 内部两部分,类似 Apple 的 CoreFoundation / CoreUI 的关系:
- core/foundation/(Core Foundation)—— 应用级非 UI 基础设施:常量、配置、异常、日志、类型、工具函数、扩展
- core/ui/(Core UI)—— UI 基础设施:基础定义(颜色/字体/间距/主题)、基础组件(按钮/输入框/头像)、业务组合组件(弹窗/面板/Toast)
多语言国际化(l10n)已提取为独立 Package(
packages/l10n_sdk),与其他 SDK 统一由 Melos 管理。
依赖方向(严格单向):core/ui/ → l10n_sdk → core/foundation/。foundation 是最底层;l10n_sdk 仅依赖 foundation(持久化语言偏好);ui 可依赖 l10n_sdk(组件内置文案)和 foundation。
core/foundation/(Core Foundation)包含:
- 常量定义(constants)
- 应用配置(config)
- 统一异常体系(errors)
- 日志系统(logger)
- 基础类型(types)
- 工具函数(utils)
- Dart 扩展(extensions)
注意:网络/存储/加密/媒体/RTC/推送/协议/原生通信/多语言国际化等 SDK 能力已提取为独立 Package(
packages/*_sdk),由 Melos 统一管理,主 App 通过pubspec.yaml引用。详见「Mono-Repo 架构」章节和下方「7.3 核心 SDK(独立 Package)」。
7.2 基础设施
Constants(core/foundation/constants/)
app_constants.dart:应用级常量(分页大小、超时时间等)db_constants.dart:数据库相关常量(表名、字段名等)socket_constants.dart:Socket 相关常量(事件名、重连策略等)api_constants.dart:API 相关常量(Base URL、端点路径等)
Config(core/foundation/config.dart)
AppConfig:编译期注入的配置聚合类,通过AppConfig.current获取当前环境配置- 每个字段用
bool.fromEnvironment/String.fromEnvironment独立读取,新增字段只需在config.json和此类各加一行 isDebug:核心环境标志(true=dev,false=prod),由 CI 脚本在打包前写入config/config.jsonfeature_flags.dart:功能开关(灰度 / A/B 实验,后续单独提取)
Errors(core/foundation/errors/)
app_exception.dart:统一异常基类network_exception.dart:网络异常(超时、断连等)database_exception.dart:数据库异常auth_exception.dart:认证异常(Token 过期、未授权等)error_mapper.dart:异常 → 错误码 / 错误键映射(UI 层通过 l10n 将错误键转为本地化文案)
Logger(core/foundation/logger/)
app_logger.dart:统一日志门面log_printer.dart:日志格式化输出log_interceptor.dart:网络请求日志拦截器
Types(core/foundation/types/)
result.dart:Result<T> 类型(Success / Failure)either.dart:Either<L, R> 类型typedefs.dart:全局类型别名unit.dart:Unit 类型(无返回值占位)
7.3 核心 SDK(独立 Package)
以下 SDK 均为 packages/ 下的 Flutter Plugin(含 Android/iOS 原生代码入口),主 App 通过 import 'package:xxx_sdk/xxx_sdk.dart' 引用。所有 SDK 遵循 Facade + Wiring 内部架构(见「SDK 内部架构」章节)。
Networks SDK(packages/networks_sdk/)
HTTP + WebSocket 客户端 SDK(Flutter Plugin 声明,无实际 Native 代码)。App 层通过 import 'package:networks_sdk/networks_sdk.dart' 引用,遵循 Facade + Wiring 内部架构。
Package 目录结构
packages/networks_sdk/
├── pubspec.yaml
├── build.yaml # @ApiRequest 代码生成器注册
└── lib/
├── networks_sdk.dart # barrel file(统一导出,见下方「导出清单」)
└── src/
├── annotations/
│ └── api_request.dart # @ApiRequest 注解定义
├── generator/
│ ├── api_request_generator.dart # build_runner 代码生成器实现
│ └── builder.dart # SharedPartBuilder 入口
├── data/
│ ├── datasources/
│ │ ├── http/
│ │ │ ├── api_client.dart # Dio REST 客户端
│ │ │ ├── token_refresh_manager.dart # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用 + 主动刷新)
│ │ │ └── interceptor/
│ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
│ │ │ ├── encryption_interceptor.dart # 请求加密 / 响应解密(预留给 cipher_guard_sdk)
│ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试 + 业务错误钩子
│ │ │ └── logging_interceptor.dart # 请求/响应日志
│ │ ├── socket/
│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream/Token 热更新/加密钩子)
│ │ └── networks_sdk_method_channel_datasource.dart # 统一执行入口(executeRequest / executeDownload)
│ ├── dto/
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 响应包装解析
│ └── repositories/
│ ├── networks_sdk_repository_impl.dart
│ └── networks_messaging_repository_impl.dart
├── domain/
│ ├── entities/
│ │ ├── api_error.dart # @freezed HTTP 错误联合类型(7 变体,含 cancelled)
│ │ ├── encrypted_request.dart # 加密请求结果数据类(path / headers / body 覆盖)
│ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
│ │ ├── socket_connection_state.dart # 连接状态 enum
│ │ ├── http_method.dart # GET / POST / PUT / DELETE / PATCH
│ │ └── api_request_type.dart # request / login / upload / stream / download
│ └── repositories/
│ ├── networks_sdk_repository.dart
│ └── networks_messaging_repository.dart
└── presentation/
├── facade/
│ ├── networks_sdk_api.dart # HTTP 公开 API 接口(含 executeDownload)
│ └── networks_messaging_api.dart # WebSocket 公开 API 接口(含 updateToken / sendBytes)
└── wiring/
├── api_config.dart # HTTP 配置(baseURL / token / 回调 / 加密 / 重试 / 主动刷新)
├── socket_config.dart # WebSocket 配置(心跳 / 重连 / 加密钩子 / 压缩)
├── network_callbacks.dart # 回调类型定义(认证 / 加密 / 业务错误 / 下载进度 / WS 加密)
├── networks_sdk_core.dart
├── networks_sdk_api_impl.dart
├── networks_messaging_api_impl.dart
└── networks_sdk_wiring.dart # 工厂:build() / buildMessagingApi()
SDK 与 App 层边界
| 职责 | SDK (networks_sdk) | App 层 (im_app) |
|---|---|---|
| Dio 管理 | ApiClient 内部创建管理 | 通过 NetworksSdkWiring.build(config:) 创建 |
| baseURL | ApiConfig.baseURL | AppConfig.apiBaseUrl 提供初始值 |
| Token 存储 | ApiConfig.token(内存) | 安全存储、持久化 |
| Token 刷新 | 检测过期 → 调 onTokenRefresh | 提供回调实现 |
| 强制登出 | 检测条件 → 调 onForceLogout | 提供回调(清状态、跳转登录) |
| 错误码定义 | 通用 code != 0 判断 | 定义具体业务码传入 |
| 请求定义 | ApiRequestable 协议 + @ApiRequest 注解 | 各 feature 实现具体 Request |
| Upload | uploadData getter + FormData/Uint8List 支持 | override uploadData + decodeResponse |
| WebSocket 连接 | SocketClient 内部管理(连接/心跳/重连) | 调 connect/disconnect/send |
| WebSocket 心跳 | 双层心跳自动管理(底层 ping 5s + 应用层 10s) | 无需关心 |
| WebSocket 重连 | 指数退避自动重连(1s→2s→4s→8s→16s→30s) | 无需关心 |
| WebSocket 生命周期 | 提供 onEnterForeground/Background | App 层调用(AppLifecycleListener)。本项目 disconnectInBackground=false,所有平台后台保活、心跳不停 |
| WebSocket 消息解析 | JSON.decode → Stream 输出 | App 层按 type 过滤 + DTO 解析 |
| Riverpod | 无依赖 | Provider 包装 NetworksSdkApi / SocketClient |
命名规范(全链路一致性)
从 Request 文件到 Domain Entity,所有文件命名必须遵循统一规则,方便区分职责和业务模块。
| 层级 | 文件命名 | 类命名 | 示例 |
|---|---|---|---|
| 接口定义 | {action}_request.dart | Request: {Action}RequestResponse DTO: {Action}Data | login_request.dart → LoginRequest + LoginData |
| 持久化 DTO | data/models/{entity}_dto.dart | {Entity}Dto | user_dto.dart → UserDto |
| Repository 接口 | domain/repositories/{module}_repository.dart | {Module}Repository | auth_repository.dart → AuthRepository |
| Repository 实现 | data/repositories/{module}_repository_impl.dart | {Module}RepositoryImpl | auth_repository_impl.dart → AuthRepositoryImpl |
| Domain Entity | domain/entities/{entity}.dart | {Entity} | user.dart → User |
| UseCase(按需) | features/{module}/usecases/{action}_usecase.dart | {Action}UseCase | login_usecase.dart → LoginUseCase |
关键规则:
- 一个端点 = 一个 Request 文件:Response DTO + Request 类放在同一文件中
- Response DTO 必须有
toEntity():统一 DTO → Domain Entity 的转换入口 - 持久化 DTO 和 Response DTO 分开:Response DTO(
XxxData)在 request 文件中,持久化 DTO(XxxDto)在data/models/ - 禁止跳层:ViewModel → Repository(→ UseCase 按需)→ NetworksSdkApi,每层职责明确
傻瓜式教程:从零开始定义并发送一个接口
前置条件(只需做一次)
打开一个独立终端窗口,在项目根目录执行以下命令并保持运行,不要关闭:
melos run gen:watch
这个命令会监听你的代码变化,每次保存 .dart 文件后自动生成 .g.dart。
整个开发期间这个终端窗口必须常驻,不要关。
以「登录接口」为完整示例。假设后端给你的接口文档是:
POST /auth/login
请求体:{ "email": "xxx", "password": "xxx" }
响应体:{ "code": 0, "message": "ok", "data": { "token": "xxx", "user_id": "123", "email": "xxx", "nickname": "xxx", "avatar": "xxx" } }
你需要做 4 步,每一步都告诉你在哪个文件写什么。
第 1 步:创建接口定义文件
在哪创建:lib/data/remote/ 目录下
文件名:login_request.dart(以端点名命名,一个端点 = 一个文件)
规则:Response DTO + Request 放在同一个文件里。打开这一个文件就能看到请求参数和响应结构。
第 1.1 步:写文件头
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import '../../../core/foundation/api_paths.dart'; // API 路径常量
import '../../../domain/entities/user.dart'; // Domain Entity(后面 toEntity 要用)
part 'login_request.g.dart'; // 这行必须写!指向即将自动生成的文件
part 'login_request.g.dart'; 写完后 IDE 会短暂报红(因为 .g.dart 还没生成)。如果 watch 模式已启动,保存文件后几秒内红线会自动消失。
第 1.2 步:写 Response DTO(服务端返回什么字段,就写什么字段)
// ── Response DTO ──
/// 服务端返回的登录数据
@JsonSerializable() // ← 这个注解让 build_runner 自动生成 fromJson / toJson
class LoginData {
final String token; // 服务端返回的字段
@JsonKey(name: 'user_id') // 服务端字段名是 user_id,Dart 字段名是 userId
final String userId;
final String email;
final String? nickname; // 可选字段用 String?
final String? avatar;
const LoginData({ // 构造函数,参数和字段一一对应
required this.token,
required this.userId,
required this.email,
this.nickname,
this.avatar,
});
// ↓ 这两行是固定写法,照抄就行,把类名替换掉
factory LoginData.fromJson(Map<String, dynamic> json) =>
_$LoginDataFromJson(json); // ← 短暂报红,watch 模式下保存后几秒自动消失
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
/// DTO → Domain Entity(把网络层数据转为业务层数据)
User toEntity() {
return User(
id: userId,
email: email,
nickname: nickname,
avatar: avatar,
);
}
}
第 1.3 步:在 ApiPaths 中添加路径常量
文件:lib/core/foundation/api_paths.dart(全局路径常量表,所有接口路径都在这里统一管理)
class ApiPaths {
ApiPaths._();
// ── Auth ──
static const authLogin = '/auth/login'; // ← 新接口在这里加一行
static const authRefreshToken = '/auth/refresh-token';
static const authLogout = '/auth/logout';
// ── User ──
static const userProfile = '/user/profile';
// ...
}
新增接口时,先在这里加路径常量,然后在 Request 中引用 ApiPaths.xxx。
好处:全局搜索 ApiPaths. 就能看到所有接口列表,路径变更只改一处。
第 1.4 步:写 Request(你要发什么参数给服务端)
// ── Request ──
@ApiRequest( // ← 这个注解让 build_runner 自动生成 path / method 等
path: ApiPaths.authLogin, // 路径常量,定义在 core/foundation/api_paths.dart
method: HttpMethod.post, // HTTP 方法,从接口文档抄
responseType: LoginData, // 响应类型,就是上面写的 LoginData
requestType: ApiRequestType.login, // login 类型不携带 Token(登录前还没有 Token)
)
@JsonSerializable() // ← 自动生成 toJson(把请求参数序列化为 JSON)
class LoginRequest extends ApiRequestable<LoginData> // ← 固定写法:extends ApiRequestable<响应类型>
with _$LoginRequestApi { // ← 固定写法:with _$类名Api(短暂报红,保存后自动消失)
final String email; // 请求参数:要发给服务端的字段
final String password;
LoginRequest({required this.email, required this.password});
@override
Map<String, dynamic> toJson() => _$LoginRequestToJson(this); // ← 固定写法,短暂报红,保存后自动消失
}
保存文件。后台的 melos run gen:watch 自动检测到变化,几秒后生成 login_request.g.dart,所有红线消失。
命名规则速查(写之前就能确定引用名)
| 你写的类名 | fromJson | toJson | Api mixin |
|---|---|---|---|
LoginData | _$LoginDataFromJson | _$LoginDataToJson | - |
LoginRequest | _$LoginRequestFromJson | _$LoginRequestToJson | _$LoginRequestApi |
SendMessageRequest | _$SendMessageRequestFromJson | _$SendMessageRequestToJson | _$SendMessageRequestApi |
规则:_$ + 类名 + FromJson / ToJson / Api。固定前缀,直接拼。
第 2 步:在 Repository 中调用 NetworksSdkApi,转为 Domain Entity
在哪写:lib/data/repositories/auth_repository_impl.dart
做什么:调 NetworksSdkApi.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。
import 'package:networks_sdk/networks_sdk.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../remote/login_request.dart';
class AuthRepositoryImpl implements AuthRepository {
final NetworksSdkApi _client; // ← 注入 Facade 接口
final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
AuthRepositoryImpl({
required NetworksSdkApi client,
required void Function(String?) onTokenUpdate,
}) : _client = client,
_onTokenUpdate = onTokenUpdate;
@override
Future<User> login({
required String email,
required String password,
}) async {
// 1. 调 NetworksSdkApi,构造请求 → 发 HTTP → 自动解码 → 返回 DTO
final LoginData? loginData = await _client.executeRequest(
LoginRequest(email: email, password: password),
);
if (loginData == null) {
throw Exception('Login failed: empty response');
}
// 2. 回调写入 Token(内存 + 持久化由 Provider 层组合)
_onTokenUpdate(loginData.token);
// 3. DTO → Domain Entity,返回给上层
return loginData.toEntity();
}
}
第 3 步:注册 Provider + 编写 ViewModel
3.1 注册 Provider(DI 装配)
在哪写:lib/features/{模块}/di/{模块}_providers.dart
做什么:在 Feature 目录下创建 Provider 文件,注册该模块的 DI 链路(Repository → UseCase 按需)。app/di/ 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 Provider 内聚在 Feature 目录下。
// ── features/auth/di/auth_providers.dart ──
// Repository(注入 Facade 接口 + 回调组合多个 SDK 能力)
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final apiConfig = ref.read(apiConfigProvider);
return AuthRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存(networks_sdk)
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
},
);
});
// UseCase(按需 — 登录有多步编排,所以需要 UseCase)
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});
新模块示例:添加「消息」模块的 Provider
假设你新建了 MessageRepositoryImpl,需要注册 Provider:
创建 lib/features/chat/di/chat_providers.dart:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/di/network_provider.dart';
import '../../../data/repositories/message_repository_impl.dart';
import '../../../domain/repositories/message_repository.dart';
// ── Repository ──
final messageRepositoryProvider = Provider<MessageRepository>((ref) {
return MessageRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
);
});
// 大多数模块只需到 Repository 这一层,ViewModel 直接调 Repository 即可。
// 如需 UseCase(多步编排、跨模块协调),参考 auth_providers.dart 中的 loginUseCaseProvider。
原则:app/di/ 只放 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路(Repository → UseCase 按需)内聚在 features/{模块}/di/{模块}_providers.dart 中。
3.2 编写 ViewModel
在哪写:lib/features/auth/presentation/login_view_model.dart
常规写法:ViewModel 直接调 Repository
@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() => const LoginState();
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true);
try {
// 直接调 Repository
final user = await ref.read(authRepositoryProvider).login(
email: email,
password: password,
);
state = state.copyWith(user: user, isLoading: false);
} on ApiError catch (e) {
state = state.copyWith(error: e.displayMessage, isLoading: false);
}
}
}
进阶写法:有 UseCase 时(如登录需格式校验 + 多步编排)
@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() => const LoginState();
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true);
try {
// 通过 UseCase 调用(格式校验 → 登录 → 返回 User)
final user = await ref.read(loginUseCaseProvider).execute(
email: email,
password: password,
);
state = state.copyWith(user: user, isLoading: false);
} on FormatException catch (e) {
// 格式校验失败(UseCase 层抛出)
state = state.copyWith(error: e.message, isLoading: false);
} on ApiError catch (e) {
// 网络错误统一处理
state = state.copyWith(error: e.displayMessage, isLoading: false);
}
}
}
View 层只需 ref.watch(loginViewModelProvider),状态变化时自动刷新 UI。
完整数据流(从用户点击按钮到 UI 刷新)
常规路径:ViewModel → Repository(大多数场景)
用户点击按钮
→ View: vm.doSomething(...)
→ ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
→ RepositoryImpl.doSomething() // data/repositories/
→ _client.executeRequest(XxxRequest) // 调 NetworksSdkApi
→ 自动注入 header → HTTP 请求 → 自动解码 → DTO
→ dto.toEntity() → Domain Entity
← state = state.copyWith(...) // 更新状态
← View: ref.watch → 自动 rebuild // 自动刷新
进阶路径:ViewModel → UseCase → Repository(登录等复杂场景)
用户点击「登录」按钮
→ View: vm.login(email, password)
→ ViewModel: ref.read(loginUseCaseProvider).execute(...)
→ LoginUseCase: 格式校验(邮箱 + 密码) // features/auth/usecases/
→ LoginUseCase: authRepository.login(...)
→ AuthRepositoryImpl.login() // data/repositories/
→ _client.executeRequest(LoginRequest) // 调 NetworksSdkApi
→ Auth → Encryption → Dio.request → Retry → Logging // 拦截器链自动处理
← request.decodeResponse → LoginData.fromJson // 自动解码
← LoginData(DTO)
→ onTokenUpdate(token) // 回调:内存写入 + 持久化
← loginData.toEntity() → User(Domain Entity)
← User
← state = state.copyWith(user: user) // 更新状态
← View: ref.watch → 自动 rebuild → UI 显示用户信息 // 自动刷新
你只写了:接口定义文件 + Repository 一个方法 + ViewModel 一个方法。
网络请求、header 注入、token 管理、响应解码、错误处理、JSON 序列化 —— 全部自动完成,你不需要写任何一行。
再来一个:发送消息接口(POST /chat/send-message)
后端接口文档:
POST /chat/send-message
请求体:{ "chat_id": "xxx", "content": "hello" }
响应体:{ "code": 0, "data": { "message_id": "456", "timestamp": 1700000000 } }
你只需创建一个文件:lib/data/remote/send_message_request.dart,然后在 Repository 中调用即可。
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
part 'send_message_request.g.dart';
// ── Response DTO ──
@JsonSerializable()
class SendMessageData {
@JsonKey(name: 'message_id')
final String messageId;
final int timestamp;
const SendMessageData({required this.messageId, required this.timestamp});
factory SendMessageData.fromJson(Map<String, dynamic> json) =>
_$SendMessageDataFromJson(json);
}
// ── Request ──
@ApiRequest(
path: ApiPaths.chatSendMessage, // 路径常量,定义在 api_paths.dart
method: HttpMethod.post, // HTTP 方法
responseType: SendMessageData, // 响应类型
// requestType 不写,默认 ApiRequestType.request(会携带 Token)
)
@JsonSerializable()
class SendMessageRequest extends ApiRequestable<SendMessageData>
with _$SendMessageRequestApi {
@JsonKey(name: 'chat_id') // JSON 字段名和 Dart 字段名不一样时用 @JsonKey
final String chatId;
final String content;
SendMessageRequest({required this.chatId, required this.content});
@override
Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
}
保存 → 自动生成 → 然后在 Repository 中调 NetworksSdkApi 就完了:
// 在 MessageRepositoryImpl 中添加
Future<SendMessageData?> sendMessage({
required String chatId,
required String content,
}) {
return _client.executeRequest(
SendMessageRequest(chatId: chatId, content: content),
);
}
再来一个:获取用户资料(GET /user/profile)
GET /user/profile ← 无 query 参数,靠 Authorization token 标识当前用户
响应体:{ "code": 0, "data": { "user_id": "123", "email": "tom@example.com", "nickname": "Tom", "avatar": "https://..." } }
// lib/data/remote/get_profile_request.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
part 'get_profile_request.g.dart';
@JsonSerializable()
class ProfileData {
@JsonKey(name: 'user_id')
final String userId;
final String email;
final String? nickname;
final String? avatar;
const ProfileData({required this.userId, required this.email, this.nickname, this.avatar});
factory ProfileData.fromJson(Map<String, dynamic> json) =>
_$ProfileDataFromJson(json);
User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
}
@ApiRequest(
path: ApiPaths.userProfile,
method: HttpMethod.get, // ← GET 请求,toJson() 结果作为 query string
responseType: ProfileData,
)
@JsonSerializable()
class GetProfileRequest extends ApiRequestable<ProfileData>
with _$GetProfileRequestApi {
GetProfileRequest(); // 无参数 — token 标识当前用户,无需显式传 user_id
@override
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
}
无响应数据的接口(POST /auth/logout)
有些接口不返回 data 字段,只有 {"code": 0, "message": "ok"}。这种情况用 ApiRequestable<void>:
// lib/data/remote/logout_request.dart
import 'package:networks_sdk/networks_sdk.dart';
// 无 Response DTO → 泛型写 void
// 不需要 @ApiRequest 注解 → 直接实现 ApiRequestable(最简写法)
class LogoutRequest extends ApiRequestable<void> {
@override
String get path => ApiPaths.authLogout;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> toJson() => {}; // 无请求体
}
Repository 调用时直接 await:
Future<void> logout() async {
await _client.executeRequest(LogoutRequest()); // 返回 null,直接 await 即可
}
文件上传 — 两种模式
上传与普通请求的核心区别:
| 对比项 | 普通请求 | Upload 请求 |
|---|---|---|
| 数据来源 | toJson() → JSON body | uploadData → FormData / Uint8List |
| requestType | request(默认) | upload |
| parameters | 有值(自动序列化) | 返回 null(SDK 自动跳过) |
| 响应解码 | 标准 { code, msg, data } | 可能需要 override decodeResponse |
模式 A:FormData 上传到自有后端
// lib/data/remote/upload_file_request.dart
@ApiRequest(
path: ApiPaths.uploadFile,
method: HttpMethod.post,
responseType: UploadResult,
requestType: ApiRequestType.upload, // ← 关键:标记为 upload
)
class UploadFileRequest extends ApiRequestable<UploadResult>
with _$UploadFileRequestApi {
final String filePath;
final String? fileName;
UploadFileRequest({required this.filePath, this.fileName});
@override
Map<String, dynamic> toJson() => {}; // upload 不走 toJson
/// FormData — SDK 通过 uploadData 获取上传数据
@override
Object? get uploadData => FormData.fromMap({
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
});
}
模式 B:二进制上传到 S3 presigned URL
先向后端获取 presigned URL,再直接上传到 S3:
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
final Uint8List data; // 二进制文件数据
final String presignedURL; // 后端返回的 S3 签名 URL
S3UploadRequest({required this.data, required this.presignedURL});
@override
String get path => presignedURL; // ← 完整 URL,SDK 检测到 http 开头不拼 baseURL
@override
HttpMethod get method => HttpMethod.put;
@override
ApiRequestType get requestType => ApiRequestType.upload;
@override
Map<String, String>? get customHeaders => {'Content-Type': 'application/octet-stream'};
@override
Map<String, dynamic> toJson() => {};
@override
Object? get uploadData => data; // Uint8List 直接作为 body
/// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 响应格式
/// 必须 override decodeResponse
@override
S3UploadResponse? decodeResponse(Response response) {
if (response.statusCode != null &&
response.statusCode! >= 200 &&
response.statusCode! < 300) {
return const S3UploadResponse(success: true);
}
return const S3UploadResponse(success: false);
}
}
HTTP 方法速查表
| 方法 | 示例接口 | 参数传递方式 | 注解 / 手写 |
|---|---|---|---|
| GET | GET /user/profile?user_id=123 | toJson() → URL query parameters | @ApiRequest |
| POST | POST /auth/login | toJson() → JSON body | @ApiRequest |
| POST(无响应) | POST /auth/logout | toJson() → JSON body → 返回 null | 手写(简单场景) |
| Upload(FormData) | POST /upload/file | uploadData → FormData | @ApiRequest + override uploadData |
| Upload(S3) | PUT presigned-url | uploadData → Uint8List | 手写 + override decodeResponse |
| PUT / PATCH | PUT /user/profile | toJson() → JSON body | @ApiRequest(同 POST) |
| DELETE | DELETE /message/:id | toJson() → JSON body 或 query | @ApiRequest 或手写 |
App 层初始化配置(已由脚手架创建,通常不需要修改)
文件位置:app/di/network_provider.dart(本文件只提供 SDK 基础设施,不放业务 Provider)
/// API 配置 Provider(全局单例)
/// baseURL 来自 config.json → --dart-define-from-file 编译注入
final apiConfigProvider = Provider<ApiConfig>((ref) {
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {
'Platform': 'Android', // TODO: 运行时从平台 API 获取
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
},
tokenExpiredCodes: {30002, 30003, 30124}, // 后端约定的 Token 过期错误码
forceLogoutCodes: {30125}, // 后端约定的强制登出错误码
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
onTokenRefresh: () async { /* 调刷新 token 接口 */ return null; },
onLog: (message, {tag}) { print('[${tag ?? 'Network'}] $message'); },
);
});
/// Networks SDK API Provider(全局单例,Facade 接口)
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
final config = ref.read(apiConfigProvider);
return NetworksSdkWiring.build(config: config);
});
DI 装配总览
app/di/ ← 手动装配:SDK 基础设施
└── network_provider.dart → apiConfigProvider + networkSdkApiProvider
features/{模块}/di/ ← 手动装配:业务模块 DI 链路(Repository → UseCase 按需)
├── auth/di/auth_providers.dart → authRepositoryProvider
│ → loginUseCaseProvider(按需)
├── chat/di/chat_providers.dart → messageRepositoryProvider
│ → sendMessageUseCaseProvider(按需)
└── ... (需要时才创建,不提前占位)
features/{模块}/presentation/ ← @riverpod 自动生成:ViewModel Provider
└── login_view_model.dart → loginViewModelProvider(.g.dart 自动生成)
di/ 目录的定位:只放需要手动装配的 Provider(构造注入、回调组合等)。ViewModel Provider 由 @riverpod 注解自动生成(写在 presentation/ 下),不在 di/ 中。
最小化原则:app/di/ 只提供 SDK 能力(ApiConfig / NetworksSdkApi),不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 features/{模块}/di/{模块}_providers.dart 中,需要时才创建。
SDK 间解耦:回调注入模式
Repository 不直接依赖 SDK 类型(如 ApiConfig)。需要 SDK 能力(如写 Token)时,通过回调注入,由 Provider 层组合多个 SDK:
// features/auth/di/auth_providers.dart — App 层是唯一知道两个 SDK 的地方
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final apiConfig = ref.read(apiConfigProvider);
// final secureStorage = ref.read(secureStorageProvider); // storage_sdk(待接入)
return AuthRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存(networks_sdk)
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
},
);
});
好处:networks_sdk 不知道 storage_sdk 的存在,AuthRepositoryImpl 也不依赖任何 SDK 类型——只接收一个 void Function(String?) 回调。各 SDK 保持独立,App 层负责组合。
错误处理
ApiError 是 Freezed 联合类型,覆盖所有网络错误场景。在 ViewModel 的 catch 中使用:
try {
final user = await ref.read(loginUseCaseProvider).execute(...);
} on ApiError catch (e) {
// 方式 A:用 displayMessage 一行搞定(ApiError 扩展方法,已内置中文提示)
showToast(e.displayMessage);
// 方式 B:精细处理每种错误
e.when(
noNetworkConnection: () => showToast('无网络连接'),
timeout: () => showToast('请求超时,请重试'),
networkError: (msg) => showToast('网络错误: $msg'),
decodingError: (msg) => showToast('数据解析失败'),
apiError: (code, msg) => showToast('服务端错误[$code]: $msg'),
cancelled: () => {}, // 用户主动取消,通常不提示
unknown: (msg) => showToast('未知错误'),
);
}
代码生成(新人必读)
@ApiRequest 与 @JsonSerializable 共享同一个 .g.dart 文件(SharedPartBuilder),无需额外配置。
开发流程(3 步)
第 0 步:启动监听(整个开发期间只需执行一次,在独立终端窗口常驻)
# ⚠️ 强制要求:开发期间必须常驻此命令
# 在项目根目录打开一个独立终端窗口,执行:
melos run gen:watch
启动后,每次保存 .dart 文件都会自动重新生成 .g.dart,无需手动操作。
第 1 步:手写源文件
// data/remote/login_request.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
part 'login_request.g.dart'; // ← 必须写,指向即将生成的文件
// ── Response DTO ──
@JsonSerializable()
class LoginData {
final String token;
final String email;
const LoginData({required this.token, required this.email});
// ↓ 此时 _$LoginDataFromJson 还不存在,IDE 会报红,正常!
factory LoginData.fromJson(Map<String, dynamic> json) =>
_$LoginDataFromJson(json);
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
}
// ── Request ──
@ApiRequest(
path: ApiPaths.authLogin,
method: HttpMethod.post,
responseType: LoginData,
requestType: ApiRequestType.login,
)
@JsonSerializable()
class LoginRequest extends ApiRequestable<LoginData>
with _$LoginRequestApi { // ← 短暂报红,保存后自动消失
final String email;
final String password;
LoginRequest({required this.email, required this.password});
@override
Map<String, dynamic> toJson() => _$LoginRequestToJson(this); // ← 短暂报红,保存后自动消失
}
第 2 步:保存文件 → 自动生成
保存后,后台的 melos run gen:watch 自动检测到文件变化,生成 login_request.g.dart。
所有红线自动消失,无需任何手动操作。
命名规则(写之前就能确定引用名)
| 注解 | 生成的符号 | 示例 |
|---|---|---|
@JsonSerializable() | _$类名FromJson() | _$LoginDataFromJson(json) |
@JsonSerializable() | _$类名ToJson() | _$LoginDataToJson(this) |
@ApiRequest(...) | _$类名Api(mixin) | _$LoginRequestApi |
规则固定,先写引用再保存,watch 模式下几秒后红线自动消失。如果红线没消失,检查 watch 终端是否在运行。
常见问题
- 忘了启动 watch:保存后红线不消失 → 检查终端是否有
melos run gen:watch在运行 - 生成报错:
melos run gen重新全量生成 - .g.dart 冲突:多人协作时 .g.dart 冲突直接删除后重新生成即可,不要手动合并
- 新增依赖后:先
dart pub get,再重启 watch
Storage SDK(packages/storage_sdk/)
纯基础设施 SDK,不感知业务表结构。遵循 Facade + Wiring 模式,结构同 cipher_guard_sdk。
职责边界:
- storage_sdk 负责:数据库连接生命周期(按 uid 隔离文件)、通用泛型 CRUD(insert / select / watch / rawQuery 等)
- im_app 负责:AppDatabase 定义(含表声明和迁移策略)、各业务表(
data/local/drift/tables/)
使用方式:
// app/di/db_provider.dart
final storageSdkProvider = Provider<StorageSdkApi>((ref) {
return StorageSdkApi(
databaseFactory: (executor) => AppDatabase(executor),
);
});
// 登录后开库
await ref.read(storageSdkProvider).openDatabase(userId);
// CRUD
final db = ref.read(storageSdkProvider);
await db.insertOrReplace(appDb.users, companion);
final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));
公开接口(StorageSdkApi):生命周期(openDatabase / closeDatabase / isDatabaseOpen)+ 泛型 CRUD(insertOrReplace / batchInsertOrReplace / updateWhere / deleteWhere / deleteAll / selectAll / selectWhere / selectFirst / watchAll / watchWhere / watchFirst / rawQuery / rawExecute / count)。
build.yaml(im_app):apps/im_app/build.yaml 配置 drift_dev|preparing_builder: generate_for: [lib/**],确保 gen:watch 在修改表文件(无注解的 .dart)时正确触发 app_database.g.dart 重新生成。
Media SDK(packages/media_sdk/)
遵循 Facade + Wiring 模式。负责图片/视频处理,具体功能实现待开发。
RTC SDK(packages/rtc_sdk/)
遵循 Facade + Wiring 模式。负责实时音视频(WebRTC),具体功能实现待开发。
Push SDK(packages/notification_sdk/)
遵循 Facade + Wiring 模式。负责推送通知(FCM / APNs),具体功能实现待开发。
Protocol SDK(packages/protocol_sdk/)
遵循 Facade + Wiring 模式。负责消息协议(Protobuf 序列化),具体功能实现待开发。
CipherGuard SDK(packages/cipher_guard_sdk/)—— Flutter Plugin
端对端加密 SDK,同时处理 Dart 侧加解密和 Native 侧密钥同步(iOS App Group 用于推送通知解密):
cipher_guard_sdk_api.dart:公开 API 接口(Facade)encryption_flutter_service.dart:RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt),含性能优化encryption_method_channel.dart:Native 密钥同步通道(iOS App Group 共享密钥供 Notification Extension 解密)- Domain 实体:
RsaKeyPair/SessionKey/EncryptedMessage/ChatEncryptionKey android/+ios/:Plugin 注册入口,原生侧实现密钥写入 App Group
加解密性能优化
encryption_flutter_service.dart 针对 IM 高频加解密场景做了四项优化:
| 优化项 | 方案 | 效果 |
|---|---|---|
| RSA 密钥生成异步化 | generateRsaKeyPairAsync 使用 Isolate.run() 在独立线程生成 |
主线程零阻塞,不卡 UI(2048-bit 约 200-500ms) |
| 派生密钥 LRU 缓存 | _derivedKeyCache(Map,上限 64 条),缓存键 = sessionKey:round:mode,满时淘汰最早条目 |
同一 round 的加解密只算一次 KDF,后续直接命中缓存 |
| Random.secure() 复用 | 静态 _secureRandom 单例,所有 IV / 随机数生成共用 |
避免每次 Random.secure() 构造开销 |
| KDF 双模式 | KdfMode.md5(默认,兼容既有数据)和 KdfMode.pbkdf2(PBKDF2-HMAC-SHA256,可配迭代次数) |
默认快速兼容,可选安全增强(防暴力破解) |
构造时可配置 KDF 模式和 PBKDF2 迭代次数:
EncryptionFlutterService(
kdfMode: KdfMode.md5, // 默认,兼容既有数据
pbkdf2Iterations: 10000, // PBKDF2 模式下的迭代次数
)
clearDerivedKeyCache() 可在 session key 轮换时手动清空缓存。
7.4 多语言国际化(packages/l10n_sdk/)
已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。
核心策略:远端动态下载,一劳永逸
翻译文案不随包发布,App 启动后自动从远端 URL 拉取最新版本并写入共享空间,Dart 层和原生层(Android / iOS)均从共享空间读取。后续只需在多语言后台追加/修改文案,无需发版,立即生效。
启动流程
- 拉取远端翻译:
l10n_loader.dart在 App 初始化阶段请求远端 URL,下载当前语言的翻译 JSON - 写入共享空间:将下载结果持久化到跨层共享目录(Documents / App Group Container),Dart 层和原生层均可读取
- 加载入口:
l10n.dart优先从共享空间读取翻译,命中则使用;未命中则回退到内置兜底文件 - 原生层同步:Android / iOS 原生代码同样从共享空间读取,与 Dart 层使用同一份翻译数据
兜底机制
- 首次启动(远端尚未下载):使用
assets/fallback_*.json内置兜底翻译,保证界面不出空白 - 网络异常:继续使用上次成功缓存的翻译,下次启动重试
- 远端返回格式错误:忽略,保持当前缓存
模块职责
l10n_loader.dart:远端拉取 + 写入共享空间,App 启动时调用一次l10n.dart:多语言访问入口,优先读共享空间,回退至内置兜底locale_provider.dart:语言切换管理 —— 当前 Locale 状态、用户偏好持久化、跟随系统语言assets/fallback_*.json:内置兜底翻译,随包发布,仅作离线保障
为什么独立 Package:国际化服务于 core/ui(组件内置文案)和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。注意:foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。
7.5 Core UI(core/ui/)
UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:
第一层:基础定义(core/ui/base/)
最底层的视觉规范定义,不含任何 Widget,只输出颜色/字体常量和 ThemeData:
colors.dart(已实现):颜色体系 —— 品牌色、语义色(success / warning / error)、中性灰阶font.dart(已实现):字体 —— TextStyle 定义 +textTheme(brightness)(统一字族/字号/行高)app_theme.dart(已实现):主题组装 —— 将以上令牌组合为 Light / Dark ThemeData- spacing / radius / shadows 等(待开发,按需添加)
第二层:基础组件(core/ui/components/)
原子级 Widget,只依赖第一层 base,不含任何业务逻辑:
app_button.dart(已实现):按钮(Primary / Secondary / Text 变体)- app_text_field / app_avatar / app_badge 等(待开发)
第三层:业务组合组件(core/ui/composites/)
由 base + components 组合而成的高阶 Widget,封装通用业务交互模式:
app_dialog.dart(已实现):确认弹窗(title / content / 确认 / 取消按钮)- app_action_sheet / app_toast / app_empty_state / app_error_view 等(待开发)
依赖方向:composites → components → base → core/foundation/(颜色/字体等可以引用 foundation 的 config)。composites 可引用 l10n_sdk(组件内置文案)。Feature 层只引用 core/ui/,不直接使用 Flutter 原生 Material 组件。
依赖链:core/ui/ → l10n_sdk → core/foundation/,严格单向,不可反向依赖。
SDK 约束与管理
使用 Melos 实现 Mono-Repo
为了有效管理多个 SDK 和保证版本一致性,我们使用 Melos + mono-repo 架构。
Melos + mono-repo 的优势
| 维度 | Melos + mono-repo | 传统 multi-repo |
|---|---|---|
| 版本一致性 | 同一个 commit 保证所有 package 兼容 | 版本靠人同步,常出现 mismatch |
| API 变更 | 编译期立即发现,马上修复 | 发版后才发现,debug 成本高 |
| Refactor 成本 | 一次性全 repo refactor | 需要跨 repo、分批跟进 |
| 依赖关系管理 | Melos 自动解析、link 本地套件 | pub / git tag 人工管理 |
| SDK 开发体验 | 改 SDK → example app 立即验证 | 必须先发版才能验证 |
| CI / 指令一致性 | 一套 melos 指令走天下 | 每个 repo 一套 script |
| 测试策略 | 可只测受影响的 packages | 常常只能全测或凭感觉 |
| Debug 效率 | 问题可回溯到单一 commit | 问题横跨多个 repo / 版本 |
| 新人上手 | clone 一个 repo 就全到位 | 要 clone 多个 repo 才能跑 |
| 技术债累积 | 缓慢、可控 | 指数型成长 |
SDK 约束规则
- 位置约束:所有可复用 SDK 在
packages/独立 Package,应用级基础设施在core/foundation/,UI 基础设施在core/ui/ - 依赖约束:SDK 之间不能相互依赖(除非明确声明)
- 职责约束:SDK 只提供纯技术能力,不包含业务逻辑
- 版本管理:使用 Melos 统一管理版本和依赖
- 独立性:每个 SDK 可独立测试、发布
环境配置、初始化步骤和 Melos 命令速查表见文档顶部 Part 0:开发环境配置。
扩展性设计
8.1 新增 Feature
添加新功能的标准流程:
- 在
features/下创建新目录 - 创建 UI 层页面
- 创建 Presentation 层 ViewModel
- 创建 Domain 层 UseCase
- 在 Domain 层定义 Repository 接口
- 在 Data 层实现 Repository
- 在
features/{模块}/di/{模块}_providers.dart中注册模块 Provider
8.2 标准 Feature 结构模板
每个新 Feature 都应遵循以下标准结构:
两档模板选择指南
| 复杂度 | 适用场景 | 必须有 | 可选添加 |
|---|---|---|---|
| 简单 | search、settings、profile 等 | view/ + presentation/(vm + state) | di/(需要自定义 Provider 时) |
| 标准 | chat、call、auth 等复杂功能 | view/ + presentation/ + di/ | usecases/(多步编排、跨模块协调时按需添加) |
简单模板(search / settings / profile 类)
features/[feature]/
├── view/
│ ├── [feature]_page.dart
│ └── widgets/
└── presentation/
├── [feature]_view_model.dart # ViewModel(直接调 Repository)
├── [feature]_view_model.g.dart # 代码生成
├── [feature]_state.dart # State(@freezed)
└── [feature]_state.freezed.dart # 代码生成
标准模板(chat / call / auth 类)
features/[feature]/
├── di/
│ └── [feature]_providers.dart # DI 装配(Repository → UseCase 按需)
├── view/
│ ├── [feature]_page.dart
│ └── widgets/
├── presentation/
│ ├── [feature]_view_model.dart
│ ├── [feature]_view_model.g.dart
│ ├── [feature]_state.dart
│ └── [feature]_state.freezed.dart
└── usecases/ # 按需 — 有多步编排时才添加
└── [action]_usecase.dart
Domain Entity 说明:共享实体(Message、Contact、User 等)统一放在全局 domain/entities/,不在 feature 内部定义。
完整示例:创建 Profile Feature(简单模板)
features/profile/] Step2[2. 创建 UI 层
profile/view/profile_page.dart] Step3[3. 创建 Presentation 层
profile/presentation/profile_view_model.dart] Step4[4. 定义 Repository 接口
domain/repositories/profile_repository.dart] Step5[5. 实现 Repository
data/repositories/profile_repository_impl.dart] Step6[6. 注册 Provider
features/profile/di/profile_providers.dart] StepOpt[可选:提取 UseCase
profile/usecases/] Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> Step6 Step3 -.-> StepOpt StepOpt -.-> Step4 style Step1 fill:#e1f5ff,stroke:#0288d1 style Step2 fill:#e1f5ff,stroke:#0288d1 style Step3 fill:#fff4e6,stroke:#f57c00 style Step4 fill:#e8f5e9,stroke:#388e3c style Step5 fill:#e8f5e9,stroke:#388e3c style Step6 fill:#fff9c4,stroke:#f57f17 style StepOpt fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5
具体步骤
步骤 1:创建 Feature 目录
lib/features/profile/
├── view/
└── presentation/
步骤 2:创建 UI 层页面(使用 ConsumerWidget)
// features/profile/view/profile_page.dart
class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 监听状态
final state = ref.watch(profileViewModelProvider);
final viewModel = ref.read(profileViewModelProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('个人资料')),
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: ProfileContent(
profile: state.profile,
onUpdate: viewModel.updateProfile,
),
);
}
}
步骤 3:创建 Presentation 层 ViewModel(直接调 Repository)
// features/profile/presentation/profile_state.dart
@freezed
class ProfileState with _$ProfileState {
const factory ProfileState({
Profile? profile,
@Default(false) bool isLoading,
@Default('') String error,
}) = _ProfileState;
}
// features/profile/presentation/profile_view_model.dart
@riverpod
class ProfileViewModel extends _$ProfileViewModel {
@override
ProfileState build() => const ProfileState();
Future<void> loadProfile() async {
state = state.copyWith(isLoading: true, error: '');
try {
// 直接调 Repository,无需 UseCase 中间层
final profile = await ref.read(profileRepositoryProvider).getProfile();
state = state.copyWith(profile: profile);
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
Future<void> updateProfile(Profile profile) async {
state = state.copyWith(isLoading: true);
try {
await ref.read(profileRepositoryProvider).updateProfile(profile);
state = state.copyWith(profile: profile);
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
}
步骤 4:在全局 Domain 层定义 Repository 接口
// domain/repositories/profile_repository.dart
abstract class ProfileRepository {
Future<Profile> getProfile();
Future<void> updateProfile(Profile profile);
}
步骤 5:在 Data 层实现 Repository
// data/repositories/profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepository {
final NetworksSdkApi _client;
ProfileRepositoryImpl({required NetworksSdkApi client})
: _client = client;
@override
Future<Profile> getProfile() async {
final data = await _client.executeRequest(GetProfileRequest());
return data!.toEntity();
}
@override
Future<void> updateProfile(Profile profile) async {
// 实现逻辑
}
}
步骤 6:在 Feature 目录下注册模块 Provider
创建 features/profile/di/profile_providers.dart,注册 DI 链路:
// features/profile/di/profile_providers.dart
import '../../../app/di/network_provider.dart';
// ── Repository ──
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
return ProfileRepositoryImpl(
client: ref.read(networkSdkApiProvider), // 注入 Facade 接口
);
});
// Profile 是简单 CRUD,不需要 UseCase。
// ViewModel 通过 @riverpod 注解自动生成 Provider,无需额外注册。
说明:Profile 属于简单模板,ViewModel 直接调 Repository,无需 UseCase 中间层。app/di/ 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路内聚在 Feature 目录下。
Feature 结构图示
profile/view/profile_page.dart] Presentation[Presentation Layer
profile/presentation/profile_view_model.dart] DI[DI 装配
profile/di/profile_providers.dart] end subgraph GlobalLayers[全局层] RepoInterface[Repository Interface
domain/repositories/profile_repository.dart] RepoImpl[Repository Implementation
data/repositories/profile_repository_impl.dart] SDKs[SDK Packages
networks_sdk / storage_sdk] end UI --> Presentation Presentation -->|直接调用| RepoInterface DI -.装配.-> RepoInterface RepoInterface -.实现.-> RepoImpl RepoImpl --> SDKs style ProfileFeature fill:#e3f2fd,stroke:#0288d1,stroke-width:3px style GlobalLayers fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
关键要点:
1. 垂直切片:每个 Feature 包含 UI → Presentation(→ UseCase 按需)的链路
2. 高内聚:Feature 内部的代码都在同一目录下,便于维护
3. 低耦合:ViewModel 通过 Repository 接口与数据层解耦
4. 渐进式:从简单模板起步,业务复杂时按需升级为标准模板
8.3 替换底层实现
由于依赖倒置原则,可以轻松替换底层实现:
- 替换数据库:只需修改 Drift SDK
- 替换网络库:只需修改 Network SDK
- 替换加密算法:只需修改 Crypto SDK
8.4 跨平台扩展
- Platform Adapters 处理平台差异
- SDK Packages 提供统一接口
- Platform-specific 实现在各自的 SDK Package 中(如 cipher_guard_sdk 的 android/ / ios/)
项目配置
10.1 pubspec.yaml 依赖
核心依赖
name: im_app
description: IM Application with Clean Architecture
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# 状态管理 - Riverpod
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
# 不可变状态 - Freezed
freezed_annotation: ^2.4.1
# JSON 序列化
json_annotation: ^4.8.1
# 依赖注入(可选,Riverpod 已包含依赖管理)
# get_it: ^7.6.0
# injectable: ^2.3.0
# 路由导航
go_router: ^12.0.0
# 网络请求
dio: ^5.4.0
web_socket_channel: ^2.4.0
# 本地存储
drift: ^2.15.0
sqlite3_flutter_libs: ^0.5.0
flutter_secure_storage: ^9.0.0
shared_preferences: ^2.2.0
# 加密
encrypt: ^5.0.3
crypto: ^3.0.3
# 媒体处理
image_picker: ^1.0.4
video_player: ^2.8.0
cached_network_image: ^3.3.0
# RTC
agora_rtc_engine: ^6.3.0
# 或者 flutter_webrtc: ^0.9.0
# 推送通知
firebase_messaging: ^14.7.0
flutter_local_notifications: ^16.3.0
# Protocol Buffers
protobuf: ^3.1.0
# 工具库
equatable: ^2.0.5
dartz: ^0.10.1
intl: ^0.18.1
dev_dependencies:
flutter_test:
sdk: flutter
# 代码生成
build_runner: ^2.4.6
riverpod_generator: ^2.3.0
freezed: ^2.4.5
json_serializable: ^6.7.1
drift_dev: ^2.15.0
# 代码检查
flutter_lints: ^3.0.0
very_good_analysis: ^5.1.0
# 测试
mocktail: ^1.0.1
integration_test:
sdk: flutter
依赖说明
| 类别 | 包名 | 用途 |
|---|---|---|
| 状态管理 | flutter_riverpod | Riverpod 核心库 |
| riverpod_annotation | Riverpod 注解,用于代码生成 | |
| 不可变状态 | freezed_annotation | Freezed 注解,生成不可变类 |
| 代码生成 | build_runner | Dart 代码生成工具 |
| riverpod_generator | Riverpod Provider 代码生成 | |
| freezed | Freezed 代码生成 | |
| json_serializable | JSON 序列化代码生成 | |
| 网络 | dio | HTTP 客户端 |
| web_socket_channel | WebSocket 通信 | |
| 存储 | drift | 类型安全的响应式数据库(基于 SQLite) |
| flutter_secure_storage | 安全存储(加密) | |
| 测试 | mocktail | Mock 测试工具 |
10.2 代码生成命令
项目使用 Melos 统一管理,所有代码生成命令通过 melos run 执行,会自动作用于所有依赖 build_runner 的 Package。
一次性生成
用于 CI 流水线,或首次 clone 后手动触发一次:
melos run gen
监听模式(开发期间必开)
开发时需要在一个独立的 Terminal 窗口中启动,全程保持运行。每次保存 .dart 文件后,build_runner 会自动检测变化并重新生成对应的 .g.dart / .freezed.dart 文件,无需手动执行。
# 在独立 Terminal 窗口执行,不要关闭
melos run gen:watch
⚠️ 若忘记开启,修改了 @freezed / @JsonSerializable 等注解后不会自动生成,编译时会报找不到对应文件的错误。
底层等价命令(参考)
Melos 实际代为执行的命令,无需手动调用:
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch --delete-conflicting-outputs
10.3 环境配置与启动入口
设计思路
采用 单一配置文件 + CI 脚本写入 的方式管理多环境配置:
config/config.json提交到 Git,默认存 dev 值(IS_DEV=true)- CI 打包线上版本时,脚本直接改写此文件写入 prod 值,再执行
flutter build - Dart 通过
--dart-define-from-file在编译期将 JSON 字段注入二进制,运行时零开销读取
config/config.json(提交到 Git,默认 dev)
{
"IS_DEV": true,
"API_BASE_URL": "https://dev-api.example.com"
}
后续新增字段(WebSocket 地址、Sentry DSN、第三方 SDK Key 等)直接在此文件加 Key 即可,无需改启动逻辑。
core/foundation/config.dart
// 编译期从 --dart-define-from-file=config/config.json 注入
// CI 打包时脚本修改 config.json 写入线上值,本地开发保持默认(IS_DEV=true)
const _kIsDebug = bool.fromEnvironment('IS_DEV', defaultValue: true);
const _kApiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://dev-api.example.com',
);
class AppConfig {
const AppConfig({
required this.isDebug,
required this.apiBaseUrl,
});
/// 根据注入的编译期常量构建配置,main.dart 唯一入口
static AppConfig get current => const AppConfig(
isDebug: _kIsDebug,
apiBaseUrl: _kApiBaseUrl,
);
final bool isDebug;
final String apiBaseUrl;
bool get isProd => !isDebug;
}
app/bootstrap.dart
void bootstrap(AppConfig config) {
WidgetsFlutterBinding.ensureInitialized();
runApp(IMApp(config: config));
}
main.dart
void main() {
bootstrap(AppConfig.current);
}
Android Studio / VSCode 运行配置
在 Android Studio 的 Run/Debug Configurations → Additional run args 中配置:
| 配置名 | Additional run args | 说明 |
|---|---|---|
im-debug | --dart-define-from-file=config/config.json --debug | 本地开发调试 |
im-release | --dart-define-from-file=config/config.json --release | 本地 Release 模式验证 |
路径 config/config.json 相对于 Flutter 模块根目录(apps/im_app/)。配置已保存在 .idea/runConfigurations/ 中,clone 后 Android Studio 可直接使用。
打包脚本
每个平台均有独立的打包脚本,统一放在 scripts/ 目录下,并通过 melos 命令调用。所有脚本均启用 --split-debug-info + --obfuscate 以减少产物体积。
| 平台 | 脚本 | melos 命令 | 用途 | 产物 |
|---|---|---|---|---|
| Android | scripts/build_android.sh |
melos run build:android:apk |
本地测试 / 内部分发 | build/app/outputs/flutter-apk/app-release.apk |
melos run build:android:aab |
Google Play 上架 | build/app/outputs/bundle/release/app-release.aab |
||
| iOS | scripts/build_ios.sh |
melos run build:ios |
App Store / 内部分发 | build/ios/ipa/im_app.ipa |
| macOS | scripts/build_macos.sh |
melos run build:macos |
— | build/macos/Build/Products/Release/im_app.app |
| Windows | scripts/build_windows.sh |
melos run build:windows |
— | build/windows/x64/runner/Release/ |
melos 调用方式
melos run build:android:apk # APK(本地测试)
melos run build:android:aab # AAB(Google Play)
melos run build:ios
melos run build:macos
melos run build:windows
# prod 打包(需设置环境变量)
PROD_API_BASE_URL=https://api.example.com melos run build:android:apk -- apk prod
PROD_API_BASE_URL=https://api.example.com melos run build:android:aab -- aab prod
PROD_API_BASE_URL=https://api.example.com melos run build:ios -- prod
体积优化说明
--split-debug-info:将 Dart 调试符号从主产物中剥离,存入build/debug-info/<platform>/,可减少 10~20 MB--obfuscate:混淆 Dart 符号名称,需配合符号表还原线上崩溃堆栈- Android 额外优化:仅编译
arm64-v8a、R8 代码压缩(isMinifyEnabled)、资源压缩(isShrinkResources) - 符号表请妥善保存,与对应版本一一对应,用于线上崩溃堆栈还原
10.4 analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
missing_required_param: error
missing_return: error
todo: ignore
linter:
rules:
# 架构规则
avoid_classes_with_only_static_members: true
prefer_final_fields: true
# 代码风格
prefer_single_quotes: true
require_trailing_commas: true
sort_child_properties_last: true
prefer_const_constructors: true
prefer_const_declarations: true
prefer_const_literals_to_create_immutables: true
prefer_final_locals: true
# 命名规则
camel_case_types: true
non_constant_identifier_names: true
constant_identifier_names: true
# 代码质量
avoid_print: true
avoid_empty_else: true
no_duplicate_case_values: true
unawaited_futures: true
avoid_unnecessary_containers: true
# 性能
avoid_function_literals_in_foreach_calls: true
prefer_collection_literals: true
# 安全性
avoid_web_libraries_in_flutter: true
10.5 CI Workflow(Gitea Actions)
文件:.github/workflows/ci.yml
name: CI
on:
# 合并 PR 后触发(branch protection 保证只有 merge 能到达这里)
push:
branches: [main, dev]
# PR 提交/更新时触发,main 和 dev 都接受 PR
pull_request:
branches: [main, dev]
jobs:
lint:
name: Lint
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter (stable)
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Install Melos
run: dart pub global activate melos
- name: Deep clean
run: melos run clean:deep
- name: Install dependencies
run: dart pub get
- name: Generate code
run: melos run gen
- name: Analyze
run: melos run analyze
步骤说明
| 步骤 | 命令 | 说明 |
|---|---|---|
| Deep clean | melos run clean:deep | 清除全平台缓存(Flutter / Android Gradle / iOS Pods / macOS Pods / Windows CMake)及所有生成文件,确保 CI 环境干净 |
| Install dependencies | dart pub get | 在根目录统一解析所有 package 依赖,生成单一 pubspec.lock(Dart pub workspace) |
| Generate code | melos run gen | 生成 .g.dart / .freezed.dart(*.g.dart 不提交,CI 每次重新生成) |
| Analyze | melos run analyze | 对所有 package 执行静态分析,lint 不通过则 PR 不可合并 |
触发规则
pull_request到main/dev:PR 提交或更新时运行,必须通过才能合并push到main/dev:PR 合并后触发(两个分支均开启 branch protection,不允许直接 push)
打包策略
打包不在自动 CI 中触发,通过 IM 管理后台手动触发打包任务。打包 workflow 单独维护,与 lint/analyze 流水线解耦。
预留 CI 能力
| 能力 | 触发时机 | 状态 | 说明 |
|---|---|---|---|
| AI 代码 Review | PR 提交 / 更新时 | 🔜 预留 | 对每个 PR 的 diff 调用 AI 接口,自动输出可读性、架构合规性、潜在问题等 Review 意见,以 PR 评论形式呈现 |
分支保护建议(Gitea → Settings → Branches)
- Require a pull request before merging
- Require status checks to pass:
Lint - Do not allow bypassing the above settings
第五部分:数据流转示例
9.1 发送消息流程(Feature 驱动)
下面展示一个完整的发送消息流程,说明数据如何在 Feature 驱动的架构中流转:
chat_page.dart participant VM as features/chat/presentation/
chat_view_model.dart participant UC as features/chat/usecases/
send_message_usecase.dart participant Repo as domain/repositories/
message_repository.dart participant RepoImpl as data/repositories/
message_repository_impl.dart participant LocalDS as data/local/
message_local_ds.dart participant SDK as networks_sdk/
NetworksSdkApi / SocketClient participant WS as WebSocket Server UI->>VM: 1. 用户点击发送按钮 VM->>UC: 2. 调用 SendMessageUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Data 层实现接口 RepoImpl->>LocalDS: 5. 先保存到本地数据库 LocalDS-->>RepoImpl: 6. 本地保存成功(设置状态:发送中) RepoImpl->>SDK: 7. 直接调 SDK 发送 SDK->>WS: 8. 发送消息到服务器 WS-->>SDK: 9. 服务器确认收到 SDK-->>RepoImpl: 10. 返回发送结果 RepoImpl->>LocalDS: 11. 更新本地消息状态(已发送) LocalDS-->>RepoImpl: 12. 更新成功 RepoImpl-->>UC: 13. 返回 Message Entity UC-->>VM: 14. 返回结果 VM-->>UI: 15. 更新 UI 状态 UI->>UI: 16. 显示消息已发送 Note over UI,WS: Feature 垂直切片:chat Feature 的完整数据流
9.2 流程说明
- 用户操作:用户在
features/chat/view/chat_page.dart点击发送按钮 - ViewModel 响应:
features/chat/presentation/chat_view_model.dart处理发送逻辑 - 调用 UseCase:ViewModel 调用
features/chat/usecases/send_message_usecase.dart - Repository 接口:UseCase 通过
domain/repositories/message_repository.dart接口调用 - Repository 实现:
data/repositories/message_repository_impl.dart实现具体逻辑 - 本地优先:先保存到
data/local/message_local_ds.dart - 网络发送:Repository 调 SDK(NetworksSdkApi / SocketClient)发送
- 服务器确认:WebSocket 服务器确认接收
- 状态更新:更新本地数据库中的消息状态
- 数据返回:层层返回,最终更新 UI
完整代码示例(Riverpod 实现)
1. UI 层(ConsumerWidget)
// features/chat/view/chat_page.dart
class ChatPage extends ConsumerWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(chatViewModelProvider);
final viewModel = ref.read(chatViewModelProvider.notifier);
return Scaffold(
body: ChatInputArea(
onSend: (content) => viewModel.sendMessage(content),
),
);
}
}
2. Presentation 层(StateNotifier)
// features/chat/presentation/chat_view_model.dart
class ChatViewModel extends StateNotifier<ChatState> {
ChatViewModel(this._sendMessageUseCase) : super(const ChatState());
final SendMessageUseCase _sendMessageUseCase;
Future<void> sendMessage(String content) async {
state = state.copyWith(isLoading: true);
try {
await _sendMessageUseCase(content);
} finally {
state = state.copyWith(isLoading: false);
}
}
}
// Provider
final chatViewModelProvider =
StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
return ChatViewModel(ref.watch(sendMessageUseCaseProvider));
});
3. Domain 层(UseCase + Provider)
// features/chat/usecases/send_message_usecase.dart
class SendMessageUseCase {
final MessageRepository _repository;
SendMessageUseCase(this._repository);
Future<Message> call(String content) {
return _repository.sendMessage(content);
}
}
// Provider
@riverpod
SendMessageUseCase sendMessageUseCase(SendMessageUseCaseRef ref) {
return SendMessageUseCase(ref.watch(messageRepositoryProvider));
}
// domain/repositories/message_repository.dart
abstract class MessageRepository {
Future<Message> sendMessage(String content);
Future<List<Message>> getMessages(String chatId);
}
4. Data 层(Repository 实现 + Providers)
// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
final MessageLocalDataSource _localDS;
final NetworksSdkApi _client; // 注入 Facade 接口
MessageRepositoryImpl(this._localDS, this._client);
@override
Future<Message> sendMessage(String content) async {
// 1. 先保存到本地
final localMessage = await _localDS.saveMessage(content, status: 'sending');
// 2. 直接调 SDK 发送到服务器
try {
final serverMessage = await _client.executeRequest(
SendMessageRequest(chatId: localMessage.chatId, content: content),
);
// 3. 更新本地状态
await _localDS.updateMessageStatus(localMessage.id, 'sent');
return serverMessage!.toEntity();
} catch (e) {
await _localDS.updateMessageStatus(localMessage.id, 'failed');
rethrow;
}
}
}
// Provider
@riverpod
MessageRepository messageRepository(MessageRepositoryRef ref) {
return MessageRepositoryImpl(
ref.watch(messageLocalDataSourceProvider),
ref.watch(networkSdkApiProvider), // 注入 Facade 接口
);
}
5. Local DataSource + Provider
// data/local/message_local_ds.dart
class MessageLocalDataSource {
final AppDatabase _db;
Future<MessageDTO> saveMessage(String content, {required String status}) {
// Drift 操作
}
}
// Provider
@riverpod
MessageLocalDataSource messageLocalDataSource(MessageLocalDataSourceRef ref) {
return MessageLocalDataSourceImpl(ref.watch(databaseProvider));
}
9.3 加载会话列表流程
chat_list_page.dart participant VM as features/chat_list/presentation/
chat_list_view_model.dart participant UC as features/chat_list/usecases/
load_chat_list_usecase.dart participant Repo as domain/repositories/
chat_repository.dart participant RepoImpl as data/repositories/
chat_repository_impl.dart participant Cache as data/cache/
cache_manager.dart participant LocalDS as data/local/
chat_local_ds.dart participant SDK as networks_sdk/
NetworksSdkApi UI->>VM: 1. 页面初始化 VM->>UC: 2. 调用 LoadChatListUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Repository 实现 RepoImpl->>Cache: 5. 检查缓存 alt 缓存命中 Cache-->>RepoImpl: 6a. 返回缓存数据 else 缓存未命中 RepoImpl->>LocalDS: 6b. 读取本地数据库 LocalDS-->>RepoImpl: 7. 返回本地数据 RepoImpl->>SDK: 8. 调 NetworksSdkApi 请求远程数据 SDK-->>RepoImpl: 9. 返回最新数据 RepoImpl->>LocalDS: 10. 更新本地数据库 RepoImpl->>Cache: 11. 更新缓存 end RepoImpl-->>UC: 12. 返回 Chat Entity 列表 UC-->>VM: 13. 返回结果 VM-->>UI: 14. 更新 UI 状态 UI->>UI: 15. 显示会话列表 Note over UI,SDK: 缓存优先策略:缓存 → 本地 → 远程
9.4 跨 Feature 交互
不同 Feature 之间通过共享的 Repository 接口交互:
关键原则:
- Feature 之间不直接依赖
- 通过共享的 Repository 接口通信
- Repository 实现在 Data 层统一管理
- 保持 Feature 的独立性和可测试性
9.5 数据同步策略
Repository 层负责协调本地和远程数据:
- 读取数据:缓存 → 本地数据库 → 远程服务器(三级缓存)
- 写入数据:先写入本地,再同步到远程(本地优先)
- 冲突解决:使用时间戳或版本号解决冲突
- 离线支持:本地数据库支持离线操作,网络恢复后自动同步
- 增量同步:只同步变更的数据,减少网络传输
9.6 层级依赖总结
页面 + 组件] FeaturePresentation[features/*/presentation/
ViewModel + 状态] FeatureDomain[features/*/domain/
UseCase + Entity] end subgraph GlobalDomain[Global Domain - 共享接口] direction TB RepoInterfaces[domain/repositories/
Repository 接口] ValueObjects[domain/value_objects/
值对象] end subgraph DataLayer[Data Layer - 全局实现] direction TB RepoImpl[data/repositories/
Repository 实现] LocalDS2[data/local/
本地数据源] DTOs[data/models/
DTO 模型] end subgraph CoreLayer[Core Layer - 主 App 内部] subgraph Foundation[core/foundation/ - 应用级基础设施] direction TB AppInfra[Constants / Config / Errors
Logger / Types / Utils / Extensions] end subgraph CoreUILayer[core/ui/ - UI 基础设施] direction TB DesignBase[base/] UIComponents[components/] UIComposites[composites/] end end subgraph PackagesLayer[SDK Packages - Melos 管理] direction TB SDKPkgs2[networks_sdk / storage_sdk / cipher_guard_sdk / l10n_sdk
media_sdk / rtc_sdk / notification_sdk
protocol_sdk] end FeatureView --> FeaturePresentation FeatureView -->|UI 复用| UIComposites FeatureView -->|本地化文案| SDKPkgs2 FeaturePresentation --> FeatureDomain FeatureDomain --> RepoInterfaces RepoInterfaces -.实现.-> RepoImpl RepoImpl --> LocalDS2 RepoImpl --> SDKPkgs2 LocalDS2 --> SDKPkgs2 UIComposites --> UIComponents UIComposites -->|组件内置文案| SDKPkgs2 UIComponents --> DesignBase style FeatureLayer fill:#e1f5ff,stroke:#0288d1,stroke-width:3px style GlobalDomain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px style DataLayer fill:#e8f5e9,stroke:#388e3c,stroke-width:3px style CoreLayer fill:#f5f5f5,stroke:#9e9e9e,stroke-width:3px style PackagesLayer fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px style Foundation fill:#fce4ec,stroke:#c2185b,stroke-width:2px style CoreUILayer fill:#fff4e6,stroke:#f57c00,stroke-width:2px
架构核心:
1. Feature 垂直切片:每个 Feature 包含 UI → Presentation → Domain 的完整链路
2. 全局 Repository 接口:domain/repositories/ 定义数据访问接口
3. 统一 Data 实现:data/ 实现所有 Repository,管理所有数据源
4. Core Foundation 支撑:core/foundation/ 提供应用级基础设施;SDK 能力由 packages/ 独立 Package 提供
5. L10n 国际化:packages/l10n_sdk 提供翻译资源和语言切换,被 core/ui 和 Feature 层单向引用
6. Core UI 统一视觉:core/ui/ 提供基础定义、基础组件和业务组合组件,Feature 层统一复用
7. 严格单向依赖:core/ui/ → l10n_sdk → core/foundation/,任何层级不可反向依赖
第六部分:企业级架构关键考虑因素
在实际的企业级 IM 应用开发中,除了基础架构设计,还需要考虑以下关键因素,这些因素直接影响系统的可扩展性、性能、可维护性和长期收益。
5.1 跨平台交互 Bridge 能力
核心理念:提前规划与其他平台的交互能力,确保 IM App 可以嵌入到各种宿主环境(企业内部平台、第三方应用等)。
Bridge 架构设计
Bridge 能力接口定义
/// Bridge 能力接口 - 抽象层
abstract class BridgeCapability {
/// 检测能力是否可用
Future<bool> isAvailable();
/// 初始化能力
Future<void> initialize();
/// 能力名称
String get name;
/// 能力版本
String get version;
}
/// Bridge 管理器 - 统一管理所有 Bridge 能力
class BridgeManager {
static final BridgeManager _instance = BridgeManager._internal();
factory BridgeManager() => _instance;
BridgeManager._internal();
final Map<String, BridgeCapability> _capabilities = {};
/// 注册能力
void registerCapability(BridgeCapability capability) {
_capabilities[capability.name] = capability;
}
/// 检测能力是否可用
Future<bool> hasCapability(String name) async {
final capability = _capabilities[name];
if (capability == null) return false;
return await capability.isAvailable();
}
/// 获取能力
T? getCapability<T extends BridgeCapability>(String name) {
return _capabilities[name] as T?;
}
}
/// 网络 Bridge 能力
class NetworkBridgeCapability implements BridgeCapability {
@override
String get name => 'network';
@override
String get version => '1.0.0';
@override
Future<bool> isAvailable() async {
// 检测宿主环境是否支持网络请求
return true;
}
@override
Future<void> initialize() async {
// 初始化网络能力
}
/// 通过 Bridge 发送网络请求
Future<Response> request(String url, {
required HTTPMethod method,
Map<String, dynamic>? data,
}) async {
// 调用宿主环境的网络能力
return await _callHost('network.request', {
'url': url,
'method': method.name,
'data': data,
});
}
}
/// 存储 Bridge 能力
class StorageBridgeCapability implements BridgeCapability {
@override
String get name => 'storage';
@override
String get version => '1.0.0';
@override
Future<bool> isAvailable() async {
return true;
}
@override
Future<void> initialize() async {}
/// 存储数据
Future<void> setItem(String key, String value) async {
await _callHost('storage.set', {'key': key, 'value': value});
}
/// 读取数据
Future<String?> getItem(String key) async {
return await _callHost('storage.get', {'key': key});
}
}
能力检测与降级策略
/// 能力检测与降级
class BridgeCapabilityChecker {
/// 检测所有必需能力
static Future<Map<String, bool>> checkRequiredCapabilities() async {
final manager = BridgeManager();
return {
'network': await manager.hasCapability('network'),
'storage': await manager.hasCapability('storage'),
'media': await manager.hasCapability('media'),
'push': await manager.hasCapability('push'),
'payment': await manager.hasCapability('payment'),
};
}
/// 根据能力启用功能
static Future<void> enableFeaturesBasedOnCapabilities() async {
final capabilities = await checkRequiredCapabilities();
// 网络能力不可用 - 降级到离线模式
if (!capabilities['network']!) {
FeatureToggle.enable('offline_mode');
}
// 推送能力不可用 - 使用轮询
if (!capabilities['push']!) {
FeatureToggle.enable('polling_mode');
}
// 支付能力不可用 - 隐藏支付功能
if (!capabilities['payment']!) {
FeatureToggle.disable('payment_feature');
}
}
}
关键价值
- 提前规划:在架构设计初期就考虑 Bridge 能力,避免后期重构
- 能力检测:运行时动态检测宿主环境能力,自动适配
- 优雅降级:能力不可用时自动降级,不影响核心功能
- 统一接口:通过 Bridge 层统一调用,屏蔽平台差异
5.2 数据获取多层策略
核心理念:通过内存缓存、热表、冷表、网络请求的多层数据获取策略,优化性能和用户体验。
数据分层架构
数据层定义
| 层级 | 存储位置 | 数据特点 | 访问速度 | 容量 | 生命周期 |
|---|---|---|---|---|---|
| L1: 内存缓存 | RAM | 最近访问、高频访问 | 极快(<1ms) | 小(100MB) | 应用运行期间 |
| L2: 热表 | Drift + 索引 | 频繁访问、近期活跃 | 快(1-10ms) | 中(1GB) | 30 天 |
| L3: 冷表 | Drift | 历史数据、低频访问 | 中(10-50ms) | 大(10GB) | 永久 |
| L4: 网络 | 远程服务器 | 最新数据、全量数据 | 慢(100-1000ms) | 无限 | 永久 |
数据获取策略实现
/// 多层数据获取策略
class DataFetchStrategy<T> {
final MemoryCache memoryCache;
final HotTableDataSource hotTable;
final ColdTableDataSource coldTable;
final NetworkDataSource network;
const DataFetchStrategy({
required this.memoryCache,
required this.hotTable,
required this.coldTable,
required this.network,
});
/// 获取数据 - 自动多层查找
Future<T?> fetch(String key) async {
// L1: 检查内存缓存
final cached = memoryCache.get<T>(key);
if (cached != null) {
_recordHit('memory', key);
return cached;
}
// L2: 检查热表
final hot = await hotTable.query<T>(key);
if (hot != null) {
_recordHit('hot_table', key);
memoryCache.set(key, hot); // 回填内存
return hot;
}
// L3: 检查冷表
final cold = await coldTable.query<T>(key);
if (cold != null) {
_recordHit('cold_table', key);
// 提升到热表(如果访问频率高)
if (await _shouldPromoteToHot(key)) {
await hotTable.insert(key, cold);
}
memoryCache.set(key, cold); // 回填内存
return cold;
}
// L4: 网络请求
try {
final data = await network.fetch<T>(key);
if (data != null) {
_recordHit('network', key);
// 同步到各层缓存
await _syncToCache(key, data);
return data;
}
} catch (e) {
_recordError('network', key, e);
}
return null;
}
/// 同步数据到缓存
Future<void> _syncToCache(String key, T data) async {
// 写入内存
memoryCache.set(key, data);
// 写入热表
await hotTable.insert(key, data);
// 写入冷表(持久化)
await coldTable.insert(key, data);
}
/// 判断是否应该提升到热表
Future<bool> _shouldPromoteToHot(String key) async {
final accessCount = await _getAccessCount(key);
return accessCount > 5; // 访问超过 5 次提升到热表
}
}
/// 内存缓存(LRU)
class MemoryCache {
final int maxSize;
final Map<String, dynamic> _cache = {};
final List<String> _accessOrder = [];
MemoryCache({this.maxSize = 1000});
T? get<T>(String key) {
if (!_cache.containsKey(key)) return null;
// 更新访问顺序
_accessOrder.remove(key);
_accessOrder.add(key);
return _cache[key] as T?;
}
void set(String key, dynamic value) {
// LRU 淘汰
if (_cache.length >= maxSize) {
final oldest = _accessOrder.removeAt(0);
_cache.remove(oldest);
}
_cache[key] = value;
_accessOrder.add(key);
}
}
/// 热表数据源(高频访问)
class HotTableDataSource {
/// 查询热表
Future<T?> query<T>(String key) async {
// SELECT * FROM hot_table WHERE key = ? AND last_access > (NOW() - 30 days)
return await _database.query('hot_table', where: 'key = ?', whereArgs: [key]);
}
/// 插入热表
Future<void> insert(String key, dynamic value) async {
await _database.insert('hot_table', {
'key': key,
'value': jsonEncode(value),
'last_access': DateTime.now().toIso8601String(),
});
}
}
/// 冷表数据源(历史数据)
class ColdTableDataSource {
/// 查询冷表
Future<T?> query<T>(String key) async {
// SELECT * FROM cold_table WHERE key = ?
return await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
}
/// 插入冷表
Future<void> insert(String key, dynamic value) async {
await _database.insert('cold_table', {
'key': key,
'value': jsonEncode(value),
'created_at': DateTime.now().toIso8601String(),
});
}
}
数据迁移策略
/// 数据迁移服务 - 热表与冷表之间的数据流动
class DataMigrationService {
/// 定期清理过期的热表数据
Future<void> cleanupHotTable() async {
// 删除 30 天未访问的数据
await _database.delete(
'hot_table',
where: 'last_access < ?',
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
);
}
/// 将热数据迁移到冷表
Future<void> migrateHotToCold() async {
final oldData = await _database.query(
'hot_table',
where: 'last_access < ?',
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
);
for (final row in oldData) {
await _database.insert('cold_table', row);
}
}
/// 提升冷数据到热表
Future<void> promoteColdToHot(String key) async {
final data = await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
if (data != null) {
await _database.insert('hot_table', data);
}
}
}
性能收益
- 响应速度:内存缓存命中率高,响应时间极快
- 网络流量:大幅减少网络请求,节省流量和电量
- 离线可用:冷表保存历史数据,离线时仍可访问
- 自动优化:根据访问频率自动调整数据存储位置
5.3 大量中间层的价值
核心理念:通过大量的中间层(抽象层、适配层、转换层),实现高度解耦、易于测试、便于替换。
中间层架构
packages/networks_sdk] RepositoryImpl --> StorageSDK[Storage SDK
packages/storage_sdk] style ViewModelAdapter fill:#fff4e6,stroke:#f57c00 style UseCase fill:#e8f5e9,stroke:#388e3c style RepositoryInterface fill:#e3f2fd,stroke:#2196f3 style RepositoryImpl fill:#f3e5f5,stroke:#7b1fa2
中间层的类型与作用
| 中间层类型 | 位置 | 作用 | 示例 |
|---|---|---|---|
| 抽象层 | Domain | 定义业务接口,隔离实现细节 | Repository 接口、UseCase 抽象类 |
| 适配层 | Data | 适配不同数据源,统一接口 | LocalDataSource、CacheManager |
| 转换层 | Data | DTO ↔ Entity 数据转换 | Mapper、Converter |
| 协议层 | Core | 封装通信协议,屏蔽底层细节 | APIRequestable、WebSocketProtocol |
| 策略层 | Core/Data | 封装算法和策略,易于替换 | CacheStrategy、RetryStrategy |
中间层实践示例
// 1. Repository 接口层(抽象层)
abstract class ChatRepository {
Future<List<Message>> getMessages(String chatId);
Future<void> sendMessage(Message message);
}
// 2. Repository 实现层(注入 Facade 接口)
class ChatRepositoryImpl implements ChatRepository {
final NetworksSdkApi client;
final LocalDataSource localDataSource;
final MessageMapper mapper;
ChatRepositoryImpl({
required this.client,
required this.localDataSource,
required this.mapper,
});
@override
Future<List<Message>> getMessages(String chatId) async {
// 先从本地获取
final localDTOs = await localDataSource.getMessages(chatId);
if (localDTOs.isNotEmpty) {
return localDTOs.map(mapper.toEntity).toList();
}
// 调 NetworksSdkApi 从远程获取
final response = await client.executeRequest(
GetMessagesRequest(chatId: chatId),
);
// 保存到本地
await localDataSource.saveMessages(response?.messages ?? []);
// 转换为 Entity
return (response?.messages ?? []).map(mapper.toEntity).toList();
}
}
// 4. 转换层(Mapper)
class MessageMapper {
Message toEntity(MessageDTO dto) {
return Message(
id: dto.id,
content: dto.content,
senderId: dto.senderId,
timestamp: DateTime.parse(dto.timestamp),
);
}
MessageDTO toDTO(Message entity) {
return MessageDTO(
id: entity.id,
content: entity.content,
senderId: entity.senderId,
timestamp: entity.timestamp.toIso8601String(),
);
}
}
// 5. 策略层
abstract class CacheStrategy {
bool shouldCache(String key);
Duration getCacheDuration(String key);
}
class MessageCacheStrategy implements CacheStrategy {
@override
bool shouldCache(String key) {
// 最近 100 条消息缓存
return true;
}
@override
Duration getCacheDuration(String key) {
return Duration(hours: 24);
}
}
中间层的价值
- 解耦:各层通过接口通信,修改实现不影响调用方
- 可测试:每层可独立测试,易于 Mock
- 可替换:底层实现可随时替换(如换数据库、换 API)
- 可扩展:新增功能只需新增实现,不修改接口
- 可维护:职责清晰,问题定位快速
5.4 系统能力划分
核心理念:将系统能力划分为基础能力、业务能力、快速响应机制、部署策略,实现高内聚低耦合。
能力划分架构
基础能力层
定义:与业务无关的通用技术能力,可复用到任何项目。
| 能力 | 说明 | SDK | 可复用性 |
|---|---|---|---|
| 网络通信 | HTTP/WebSocket/gRPC | NetworkSDK | 完全通用 |
| 数据存储 | Drift/SharedPreferences/SecureStorage | StorageSDK | 完全通用 |
| 端对端加密 | RSA/AES 双层加密 + Native 密钥同步 | CipherGuardSDK | 完全通用 |
| 媒体处理 | 图片/视频/音频压缩 | MediaSDK | 高度通用 |
| 音视频通话 | WebRTC/Agora | RTCSDK | 较为通用 |
| 推送通知 | FCM/APNs/本地通知 | NotificationSDK | 高度通用 |
| 推送解密 | iOS App Group 密钥同步(Notification Extension) | CipherGuardSDK | 高度通用 |
| 协议序列化 | Protocol Buffers/JSON | ProtocolSDK | 完全通用 |
业务能力层
定义:IM 领域特定的业务能力,基于基础能力层构建。
| 能力 | 说明 | 依赖基础能力 | 可复用性 |
|---|---|---|---|
| 聊天功能 | 单聊/群聊/消息管理 | Network + Storage + Protocol | IM 领域高度复用 |
| 联系人管理 | 好友/黑名单/通讯录 | Network + Storage | IM 领域高度复用 |
| 群组管理 | 创建群/成员管理/权限 | Network + Storage | IM 领域高度复用 |
| 音视频通话 | 一对一/多人通话 | RTC + Network | IM 领域较为复用 |
| 消息推送 | 离线推送/在线推送 | Notification + Network | IM 领域高度复用 |
快速响应机制
目标:通过智能缓存、预加载、性能优化,实现极速响应用户操作。
/// 快速响应管理器
class FastResponseManager {
/// 智能缓存 - 预测用户行为并缓存
Future<void> smartCache() async {
// 预缓存最近联系人的头像
final recentContacts = await _getRecentContacts();
for (final contact in recentContacts) {
await ImageCache.precache(contact.avatar);
}
// 预缓存最近会话的最后几条消息
final recentChats = await _getRecentChats();
for (final chat in recentChats) {
await MessageCache.precache(chat.id, limit: 20);
}
}
/// 预加载 - 提前加载可能访问的数据
Future<void> preload() async {
// 预加载用户资料
await UserProfilePreloader.preload();
// 预加载表情包
await EmojiPreloader.preload();
// 预加载常用设置
await SettingsPreloader.preload();
}
/// 性能优化 - 优化关键路径
Future<void> optimize() async {
// 延迟加载非关键资源
await LazyLoader.load();
// 分帧渲染大列表
await ListOptimizer.optimize();
// 图片懒加载
await ImageLazyLoader.setup();
}
}
/// 响应时间监控
class ResponseTimeMonitor {
static void track(String operation, Duration duration) {
final ms = duration.inMilliseconds;
// 记录慢操作
if (ms > 100) {
Logger.warn('Slow operation: $operation took ${ms}ms');
}
// 上报性能数据
Analytics.track('response_time', {
'operation': operation,
'duration_ms': ms,
});
}
}
容器化部署?
潜在方向:可考虑通过容器化实现快速部署、弹性扩展、版本管理。
- Docker 容器:应用容器化打包
- Kubernetes 编排:容器编排和管理
- CI/CD 流水线:自动化部署流程
- 弹性扩展:根据负载自动扩容缩容
注:容器化部署更多适用于后端服务,对于移动端 App 的应用场景需要进一步评估。
系统能力划分的价值
- 清晰边界:基础能力与业务能力分离,职责明确
- 高复用性:基础能力可复用到其他项目
- 快速响应:智能缓存和预加载提升用户体验
- 灵活部署:支持多种部署方式,适应不同场景
- 易于维护:能力独立开发和测试
5.5 严格的 Code Review 机制
核心理念:通过严格的代码审查机制,保证代码质量、一致性和可维护性。
Code Review 流程
Code Review 检查清单
| 类别 | 检查项 | 重要性 |
|---|---|---|
| 架构合规 | 是否遵循分层架构?是否违反依赖规则? | 必须 |
| 代码规范 | 是否符合命名规范?是否通过 Lint 检查? | 必须 |
| 设计原则 | 是否符合 SOLID 原则?是否高内聚低耦合? | 必须 |
| 测试覆盖 | 是否编写单元测试?测试覆盖率是否充分? | 必须 |
| 性能 | 是否有性能问题?是否有内存泄漏? | 重要 |
| 安全性 | 是否有安全漏洞?敏感数据是否加密? | 必须 |
| 可读性 | 代码是否易于理解?是否有必要的注释? | 重要 |
| 可维护性 | 是否易于修改?是否有重复代码? | 重要 |
自动化检查工具
# analysis_options.yaml - Dart 代码检查配置(同 10.4 节)
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
missing_required_param: error
missing_return: error
todo: ignore
linter:
rules:
# 架构规则
avoid_classes_with_only_static_members: true
prefer_final_fields: true
# 代码风格
prefer_single_quotes: true
require_trailing_commas: true
prefer_const_constructors: true
prefer_const_declarations: true
prefer_final_locals: true
# 命名规则
camel_case_types: true
non_constant_identifier_names: true
constant_identifier_names: true
# 代码质量
avoid_print: true
avoid_empty_else: true
no_duplicate_case_values: true
unawaited_futures: true
# 性能
avoid_function_literals_in_foreach_calls: true
prefer_collection_literals: true
# 安全性
avoid_web_libraries_in_flutter: true
Code Review 最佳实践
/// Code Review 检查工具
class CodeReviewChecker {
/// 架构合规检查
static List<String> checkArchitectureCompliance(String filePath) {
final issues = <String>[];
// 检查分层依赖
if (_hasReverseDependency(filePath)) {
issues.add('发现反向依赖:Domain 层不能依赖 Data 层');
}
// 检查跨层调用
if (_hasCrossLayerCall(filePath)) {
issues.add('发现跨层调用:UI 层不能直接调用 Repository');
}
return issues;
}
/// 代码质量检查
static List<String> checkCodeQuality(String filePath) {
final issues = <String>[];
// 检查类复杂度
if (_getClassComplexity(filePath) > 10) {
issues.add('类复杂度过高,建议拆分');
}
// 检查方法长度
if (_getMaxMethodLength(filePath) > 50) {
issues.add('方法过长,建议拆分');
}
// 检查重复代码
if (_hasDuplicateCode(filePath)) {
issues.add('发现重复代码,建议抽取');
}
return issues;
}
}
Code Review 的价值
- 提前发现问题:在代码合并前发现 Bug 和设计问题
- 保证质量:确保代码符合架构规范和编码标准
- 知识共享:团队成员相互学习和成长
- 统一风格:保持代码库的一致性
- 降低维护成本:高质量代码更易于维护
5.6 长期收益分析
核心理念:架构设计不是为了短期收益,而是为了长期可持续发展。
短期 vs 长期对比
| 维度 | 短期方案 | 长期方案(本架构) | 长期对比 |
|---|---|---|---|
| 初期开发 | 快速 | 需要更多时间 | - |
| 维护成本 | 高(大量时间修 Bug) | 低(少量维护) | 大幅降低 |
| Bug 率 | 频繁出现 Bug | Bug 很少 | 显著减少 |
| 新功能开发 | 缓慢 | 快速 | 明显提速 |
| 技术债务 | 严重 | 很少 | 避免重构 |
| 团队效率 | 低(大量救火) | 高(专注开发) | 显著提升 |
| 代码质量 | 差(难以维护) | 优(易于维护) | 可持续发展 |
长期收益体现
初期投入 vs 长期回报:
- 初期阶段:需要更多时间进行架构设计和基础设施建设
- 维护阶段:维护成本大幅降低,团队可以专注新功能开发
- 迭代阶段:新功能开发速度显著提升,架构优势逐渐显现
- 长期发展:技术债务少,代码库健康,可持续发展
收益来源:
- 维护成本降低:良好的架构设计减少维护工作量
- Bug 率降低:严格的分层和测试机制减少 Bug 数量
- 开发效率提升:清晰的架构和丰富的基础组件加速开发
- 避免重构:提前规划避免后期大规模重构
长期收益的关键指标
长期收益总结
- 投资回报:初期投入更多时间,长期回报远超初期投入
- 维护成本:大幅降低,团队可以专注创新而非救火
- Bug 率:显著减少,用户体验持续提升
- 开发效率:新功能开发明显提速,快速响应市场
- 技术债务:避免大规模重构,保持代码库健康
- 团队成长:规范的架构让团队成员快速成长
- 竞争优势:更快的迭代速度,更高的产品质量
5.7 日志与监控系统
核心理念:通过完善的日志系统和运行监控,实现问题快速定位、性能实时追踪、用户行为分析。
5.7.1 日志系统设计
日志分级策略:
| 级别 | 用途 | 输出位置 | 保留时间 |
|---|---|---|---|
| Debug | 开发调试信息 | 仅开发环境控制台 | 不保存 |
| Info | 正常运行信息、关键操作 | 本地文件 | 7 天 |
| Warning | 潜在问题、异常情况 | 本地文件 + 远程上报 | 30 天 |
| Error | 错误信息、异常堆栈 | 本地文件 + 远程立即上报 | 90 天 |
| Fatal | 严重错误、崩溃 | 本地文件 + 远程立即上报 + 告警 | 永久 |
日志分类:
5.7.2 日志系统实现
/// 日志系统 - 统一日志管理
class LoggerService {
static final LoggerService _instance = LoggerService._internal();
factory LoggerService() => _instance;
LoggerService._internal();
/// 日志级别
void debug(String message, {Map<String, dynamic>? data}) {
if (!kReleaseMode) {
_log(LogLevel.debug, message, data);
}
}
void info(String message, {Map<String, dynamic>? data}) {
_log(LogLevel.info, message, data);
_saveToLocal(LogLevel.info, message, data);
}
void warning(String message, {Map<String, dynamic>? data}) {
_log(LogLevel.warning, message, data);
_saveToLocal(LogLevel.warning, message, data);
_uploadToRemote(LogLevel.warning, message, data);
}
void error(String message, {
Map<String, dynamic>? data,
StackTrace? stackTrace,
}) {
_log(LogLevel.error, message, data);
_saveToLocal(LogLevel.error, message, data, stackTrace);
_uploadToRemote(LogLevel.error, message, data, stackTrace, immediate: true);
}
void fatal(String message, {
Map<String, dynamic>? data,
StackTrace? stackTrace,
}) {
_log(LogLevel.fatal, message, data);
_saveToLocal(LogLevel.fatal, message, data, stackTrace);
_uploadToRemote(LogLevel.fatal, message, data, stackTrace, immediate: true);
_triggerAlert(message, data, stackTrace);
}
/// 网络日志
void logNetworkRequest(String url, {
required HTTPMethod method,
Map<String, dynamic>? headers,
Map<String, dynamic>? params,
}) {
info('Network Request', data: {
'url': url,
'method': method.name,
'headers': _sanitizeHeaders(headers),
'params': _sanitizeData(params),
'timestamp': DateTime.now().toIso8601String(),
});
}
void logNetworkResponse(String url, {
required int statusCode,
required Duration duration,
String? error,
}) {
final level = statusCode >= 400 ? LogLevel.warning : LogLevel.info;
_log(level, 'Network Response', {
'url': url,
'statusCode': statusCode,
'duration': '${duration.inMilliseconds}ms',
'error': error,
});
}
/// 用户操作日志
void logUserAction(String action, {Map<String, dynamic>? data}) {
info('User Action: $action', data: data);
}
/// 性能日志
void logPerformance(String operation, Duration duration) {
if (duration.inMilliseconds > 100) {
warning('Slow Operation: $operation', data: {
'duration': '${duration.inMilliseconds}ms',
});
}
}
/// 隐私数据脱敏
Map<String, dynamic>? _sanitizeData(Map<String, dynamic>? data) {
if (data == null) return null;
final sanitized = Map<String, dynamic>.from(data);
// 脱敏敏感字段
final sensitiveKeys = ['password', 'token', 'secret', 'phone', 'email'];
for (final key in sensitiveKeys) {
if (sanitized.containsKey(key)) {
sanitized[key] = '***';
}
}
return sanitized;
}
}
enum LogLevel { debug, info, warning, error, fatal }
5.7.3 运行监控系统
监控维度:
| 监控类型 | 监控指标 | 告警阈值 | 采集频率 |
|---|---|---|---|
| 性能监控 | CPU 占用率、内存占用、帧率(FPS) | 内存 > 500MB FPS < 50 |
实时 |
| 错误监控 | Crash 率、ANR 率、异常捕获 | Crash 率 > 0.1% | 实时 |
| 网络监控 | 请求成功率、响应时间、流量消耗 | 成功率 < 95% 响应时间 > 3s |
每次请求 |
| 业务监控 | 消息发送成功率、登录成功率 | 成功率 < 98% | 每次操作 |
| 用户行为 | 页面访问路径、功能使用频率 | - | 每次操作 |
监控系统架构:
5.7.4 监控系统实现
/// 监控服务
class MonitorService {
static final MonitorService _instance = MonitorService._internal();
factory MonitorService() => _instance;
MonitorService._internal();
/// 性能监控
void trackPerformance() {
// 监控内存占用
final memoryUsage = _getMemoryUsage();
if (memoryUsage > 500 * 1024 * 1024) { // 500MB
LoggerService().warning('High Memory Usage', data: {
'memory': '${memoryUsage ~/ (1024 * 1024)}MB',
});
}
// 监控帧率
WidgetsBinding.instance.addTimingsCallback((timings) {
for (final timing in timings) {
final fps = 1000 / timing.totalSpan.inMilliseconds;
if (fps < 50) {
LoggerService().warning('Low FPS', data: {'fps': fps.toStringAsFixed(1)});
}
}
});
}
/// 错误监控 - Crash 捕获
void setupErrorMonitoring() {
// Flutter 错误捕获
FlutterError.onError = (details) {
LoggerService().fatal('Flutter Error', data: {
'exception': details.exception.toString(),
'stackTrace': details.stack.toString(),
});
};
// Dart 错误捕获
PlatformDispatcher.instance.onError = (error, stack) {
LoggerService().fatal('Dart Error', data: {
'error': error.toString(),
'stackTrace': stack.toString(),
});
return true;
};
}
/// 网络监控
void trackNetworkRequest({
required String url,
required DateTime startTime,
required DateTime endTime,
required int statusCode,
String? error,
}) {
final duration = endTime.difference(startTime);
// 记录请求
_recordMetric('network.request', {
'url': url,
'duration': duration.inMilliseconds,
'statusCode': statusCode,
'success': statusCode >= 200 && statusCode < 300,
'error': error,
});
// 慢请求告警
if (duration.inSeconds > 3) {
LoggerService().warning('Slow Network Request', data: {
'url': url,
'duration': '${duration.inSeconds}s',
});
}
// 请求失败告警
if (statusCode >= 400) {
LoggerService().error('Network Request Failed', data: {
'url': url,
'statusCode': statusCode,
'error': error,
});
}
}
/// 业务监控
void trackBusinessEvent(String event, {
bool success = true,
Map<String, dynamic>? data,
}) {
_recordMetric('business.$event', {
'success': success,
...?data,
});
if (!success) {
LoggerService().warning('Business Event Failed: $event', data: data);
}
}
/// 用户行为监控
void trackUserBehavior(String page, String action, {Map<String, dynamic>? data}) {
_recordMetric('behavior', {
'page': page,
'action': action,
'timestamp': DateTime.now().toIso8601String(),
...?data,
});
}
/// 记录指标
void _recordMetric(String metric, Map<String, dynamic> data) {
// 本地缓存
_cacheMetric(metric, data);
// 批量上报(每 1 分钟或累计 100 条)
_scheduleUpload();
}
/// 批量上报
Future<void> _scheduleUpload() async {
// 实现批量上报逻辑
}
}
/// 使用示例
class ChatViewModel extends StateNotifier<ChatState> {
Future<void> sendMessage(String content) async {
final startTime = DateTime.now();
try {
await _sendMessageUseCase(content);
// 监控业务成功
MonitorService().trackBusinessEvent('send_message', success: true, data: {
'messageLength': content.length,
'duration': DateTime.now().difference(startTime).inMilliseconds,
});
// 监控用户行为
MonitorService().trackUserBehavior('chat', 'send_message');
} catch (e, stackTrace) {
// 监控业务失败
MonitorService().trackBusinessEvent('send_message', success: false, data: {
'error': e.toString(),
});
LoggerService().error('Send Message Failed', data: {
'error': e.toString(),
}, stackTrace: stackTrace);
rethrow;
}
}
}
5.7.5 日志与监控最佳实践
| 实践 | 说明 | 价值 |
|---|---|---|
| 结构化日志 | 使用 JSON 格式记录日志,便于检索和分析 | 快速定位问题 |
| 隐私保护 | 敏感数据脱敏(密码、Token、手机号) | 符合隐私合规 |
| 批量上报 | 本地缓存,定时批量上报,减少网络请求 | 节省流量和电量 |
| 实时告警 | Fatal 级别错误立即上报并触发告警 | 及时发现严重问题 |
| 性能优化 | 日志写入异步化,不阻塞主线程 | 不影响用户体验 |
| 存储管理 | 本地日志定期清理,避免占用过多空间 | 节省存储空间 |
日志与监控的核心价值
- 问题定位:通过完整的日志链路,快速定位问题根因
- 性能优化:实时监控性能指标,及时发现和解决性能瓶颈
- 用户体验:监控用户行为,优化产品功能和交互
- 故障预警:实时告警机制,在问题扩散前及时处理
- 数据驱动:基于监控数据做决策,而非主观猜测
第七部分:总结
架构优势
- Feature 驱动的垂直切片:每个功能页面独立成模块,包含完整的 UI → Presentation → Domain 链路
- 清晰的职责划分:四层架构(Feature/Domain/Data/Core),每层有明确的职责边界
- 高度可测试:每层可独立测试,Feature 可独立验证
- 易于维护:模块化设计,修改影响范围小,功能高度内聚
- 可扩展性强:添加新 Feature 不影响现有代码,遵循标准模板
- 技术栈独立:底层实现可随时替换,业务逻辑不受影响
- 团队协作友好:Feature 驱动便于并行开发,减少代码冲突
- 低耦合高内聚:Feature 之间通过 Repository 接口解耦
核心架构设计
最佳实践
- 严格遵守分层依赖规则:单向向下依赖,禁止反向依赖和跨层调用
- Feature 垂直切片:每个 Feature 包含 view/presentation/domain 的完整链路
- 全局 Repository 接口:所有 Repository 接口定义在 domain/repositories/
- 统一 Data 实现:所有 Repository 实现在 data/repositories/
- UseCase 单一职责:每个 UseCase 只处理一个业务场景
- Riverpod 状态管理:使用 StateNotifier 管理 UI 状态,Providers 管理依赖
- 使用代码生成:利用 riverpod_generator 和 freezed 减少样板代码
- 使用 Melos 管理依赖:Mono-Repo 保证版本一致性
- 编写完整测试:单元测试、集成测试、端到端测试
- UI 层使用 ConsumerWidget:通过 ref.watch 监听状态,ref.read 读取 Provider
关键原则
Feature 驱动:以页面为单位组织代码,每个 Feature 是完整的垂直切片
依赖倒置:高层模块不依赖低层模块,两者都依赖抽象(Repository 接口)
单一职责:每个模块只做一件事,UseCase/ViewModel/Repository 各司其职
第八部分:UI 设计规范
本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。
8.0 核心约定
全局只有一份
颜色、字体、基础组件、业务弹框、图片、图标——Figma 里每种元素只有一个定义。没有"备用版本",没有"临时副本",不允许两个"差不多一样"的组件并存。
- Figma 命名是重中之重:点中任何元素都必须看到抽象名称。六类无例外:
颜色— 如primary、surface字体— 如Body/Medium、Label/Small基础组件— 如Button/Primary、Input/Default,全局只有一个版本业务弹框— 如Dialog/Confirm图片— Figma 统一导出,代码侧AppAssets注册,不硬编码路径图标— 如send、more_options,代码侧AppIcons调用
- 基础组件定稿后不随意改动:需改动时必须先告知研发,评估影响范围,双方同步后再执行
- UI 团队自主维护 UI 基建体系:研发照着 Figma 名字封装,名称必须完全一致
- 所有元素遵循同一套命名规则:新增任何元素先在 Figma 定名,研发用相同名字注册
8.1 颜色体系
所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。
语义色(随主题变化)
| 抽象名 | Figma 名 | 亮色 | 暗色 | 用途 |
|---|---|---|---|---|
primary | Primary | #2F80ED | #5BA3F5 | 主操作、链接、选中态 |
background | Background | #F8F9FA | #202124 | 页面底色 |
surface | Surface | #FFFFFF | #3C4043 | 卡片、弹框、输入框 |
onSurface | On Surface | #202124 | #FFFFFF | surface 上的文字 |
error | Error | #EB5757 | 错误状态 | |
success | Success | #27AE60 | 成功状态 | |
warning | Warning | #F2C94C | 警告状态 | |
灰阶(固定值,不随主题变化)
| 名称 | 色值 | 名称 | 色值 | 名称 | 色值 |
|---|---|---|---|---|---|
| white | #FFFFFF | gray50 | #F8F9FA | gray100 | #F1F3F4 |
| gray200 | #E8EAED | gray400 | #BDC1C6 | gray600 | #80868B |
| gray800 | #3C4043 | gray900 | #202124 | black | #000000 |
使用原则:需随主题切换 → 用语义色(primary、surface);亮暗保持不变 → 用灰阶固定值。
8.2 字体体系
字体按层级分五档:Display、Headline、Title、Body、Label,每档三个尺寸。Figma 中按 层级/尺寸 格式命名(如 Body/Large),开发用同名变量调用。
| Figma 名称 | 字号 | 字重 | 行高 | 字距 | 典型用途 |
|---|---|---|---|---|---|
| DISPLAY | |||||
| Display/Large | 57 | 400 | 64 | -0.25 | 启动页大标题、空状态 |
| Display/Medium | 45 | 400 | 52 | — | — |
| Display/Small | 36 | 400 | 44 | — | — |
| HEADLINE | |||||
| Headline/Large | 32 | 400 | 40 | — | 页面主标题、导航栏 |
| Headline/Medium | 28 | 400 | 36 | — | — |
| Headline/Small | 24 | 400 | 32 | — | — |
| TITLE | |||||
| Title/Large | 22 | 500 | 28 | — | 会话列表名称、设置项标题 |
| Title/Medium | 16 | 500 | 24 | 0.15 | 卡片标题、列表主行 |
| Title/Small | 14 | 500 | 20 | 0.1 | — |
| BODY | |||||
| Body/Large | 16 | 400 | 24 | 0.5 | 聊天气泡、表单输入 |
| Body/Medium | 14 | 400 | 20 | 0.25 | 正文说明、列表副行 |
| Body/Small | 12 | 400 | 16 | 0.4 | 辅助信息、提示文字 |
| LABEL | |||||
| Label/Large | 14 | 500 | 20 | 0.1 | 按钮文字、Tab 标签、Badge |
| Label/Medium | 12 | 500 | 16 | 0.5 | 次要标签、徽标文字 |
| Label/Small | 11 | 500 | 16 | 0.5 | 最小粒度标签 |
| 语义样式 | |||||
| Section Label | 13 | 600 | — | 0.5 | 列表分组标题、设置分区 |
| Body/Muted | 12 | 400 | 16 | — | 说明文字(灰色,低对比度) |
| Body/Error | 12 | 400 | 16 | — | 表单错误提示(红色) |
| Label/Muted | 12 | 500 | 16 | — | 时间戳、元数据(低对比度) |
8.3 组件 — Button
按钮共四种变体,每种有明确使用场景和 Figma 组件名。每个页面上主操作只用一个 Primary。
| Figma 组件名 | 用途 | 亮色样式 | 暗色样式 | 状态 |
|---|---|---|---|---|
Button/Primary | 主操作(登录、发送、确认),每屏最多一次 | 背景 #2F80ED,白字 | 背景 #5BA3F5,白字 | 默认 / Loading / 禁用(#BDC1C6) |
Button/Secondary | 次要操作(注册、稍后再说),描边样式 | 描边 #2F80ED,蓝字 | 描边 #5BA3F5,蓝字 | 默认 / 禁用 |
Button/Text | 辅助链接(忘记密码、查看全部、弹框取消) | 无背景,蓝字 | 无背景,蓝字 | 默认 |
Button/Inverse | 反色按钮(深色背景高亮),支持左侧图标 | 背景 #202124,白字 | 背景 #FFFFFF,黑字 | 默认 |
8.4 业务弹框 — Dialog
当前封装一种通用确认弹框 Dialog/Confirm,后续新增先在 Figma 以 Dialog/ 前缀命名。
| 项目 | 说明 |
|---|---|
| 结构 | 标题(不超 15 字) + 内容 + 操作区(取消 Text 样式 | 确认 Primary 样式) |
| 可配置 | 标题文字、内容文字、确认/取消标签、点击背景是否可关闭 |
| 返回值 | 确认 / 取消 / 关闭(点击背景) |
8.5 图标规范
- UI 先命名,开发跟随:Figma 确定名称,开发用完全相同的名称封装到
AppIcons - 名称有实际语义:全小写下划线,如
send、add_contact、more_options。不用拼音,不缩写 - 统一用 AppIcons 调用:不允许直接用裸 icon 库变量,替换时改一处全局生效
- 同义图标只保留一个:同功能图标在整个产品内只存在一种
新增图标流程:设计师 Figma 确认名称 → 告知开发 → 开发用相同名称在 AppIcons 注册。两端名称必须完全一致。