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 客户端
+│ │ │ │ └── interceptor/
+│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
+│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
+│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
+│ │ │ └── socket/
+│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream)
+│ │ ├── 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 错误联合类型
+│ │ │ ├── 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
+│ │ └── 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 # 回调类型定义
+│ ├── 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 会导致多次重建 +
- 无法精确控制重建范围 +
实际案例(UUTalk):
+ +// 整个 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,类型不匹配!
+
+
+运行时错误案例(UUTalk 实际问题):
+-
+
- 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
+ │ ├─ apiClientProvider
+ │ └─ messageLocalDataSourceProvider
+ └─ sendMessageUseCaseProvider
+ └─ chatRepositoryProvider
+
+
+编译期保证的价值:
+-
+
- 大幅减少 Bug:运行时错误显著下降 +
- 提升开发效率:不需要运行才知道对错 +
- 重构安全:IDE 自动提示依赖变化 +
- 团队协作:依赖关系明确,不会互相影响 +
GetX + Obx vs Riverpod:来自 UUTalk 项目的实践教训
+ +真实案例警示
+以下内容基于 UUTalk 项目的实际经验,展示了 GetX + Obx 在大型项目中暴露的严重问题。
+1. 状态管理混乱
+ +UUTalk 项目的 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. 过度嵌套和性能问题
+ +UUTalk 项目的 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 过于臃肿
+ +UUTalk 项目的巨型 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. 没有编译时安全
+ +UUTalk 项目的运行时陷阱:
+ +// 通过字符串查找 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. 难以测试
+ +UUTalk 项目的测试困境:
+ +// 测试时必须初始化整个 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 实际对比(基于 UUTalk 项目经验)
+ +| 技术维度 | +GetX + Obx | +Riverpod | +技术优势 | +
|---|---|---|---|
| 刷新颗粒度 | +Obx 包裹的整个 Widget 无法精确控制范围 |
+精确依赖追踪 支持 select 细粒度订阅 |
+大幅减少不必要 rebuild | +
| 性能基准 | +状态更新较慢 内存占用较高 |
+状态更新快速 内存占用较低 |
+性能明显提升 内存显著减少 |
+
| 数据流 | +双向绑定 状态可在任意位置修改 |
+单向数据流 状态只能通过 ViewModel 修改 |
+数据流清晰可追踪 便于状态历史回溯 |
+
| 编译期安全 | +运行时查找 Controller 类型错误运行时才发现 |
+编译期类型检查 依赖不存在立即报错 |
+大幅减少 Bug 提升开发效率 |
+
| 代码生成 | +不支持 | +riverpod_generator freezed 全面支持 |
+大幅减少样板代码 显著提升开发效率 |
+
| 依赖管理 | +全局 Get.put/find 依赖关系不透明 |
+Provider 依赖图 编译期验证依赖 |
+依赖关系可视化 重构安全有保障 |
+
| 测试支持 | +需要初始化 GetX 框架 无法 Mock 全局依赖 |
+Provider 可轻松覆盖 纯函数易于测试 |
+测试更容易 运行速度更快 |
+
| DevTools | +基础日志 | +完整依赖图 状态历史回溯 性能分析 |
+Debug 效率显著提升 | +
真实案例对比
+ +GetX + Obx(UUTalk 现状)
+ +状态混乱:
+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);
+ }
+}
+UUTalk 项目的痛点总结
+ +从 UUTalk 项目的实际经验中,我们总结出 GetX + Obx 的核心问题:
+-
+
- "快速开发"变成"技术债":初期确实快,但 6 个月后代码无法维护 +
- "响应式"变成"性能杀手":Obx 嵌套导致过度重建,页面卡顿 +
- "灵活"变成"混乱":没有约束,代码风格千差万别 +
- "全局管理"变成"依赖噩梦":牵一发动全身,不敢重构 +
- "简单上手"变成"难以精通":团队成员写出的代码质量参差不齐 +
Riverpod 的技术优势
+基于 UUTalk 项目的实践教训,我们选择 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 dio = ref.read(apiClientProvider);
+ final config = ref.read(aPIConfigurationProvider);
+
+ // 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 自动生成 path / method / requestType / includeToken / fromJson 注册
+
+@ApiRequest(
+ path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
+ 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);
+}
+
+
+// 使用 - 超级简单!
+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 + @JsonSerializable | +字段 + 构造函数 + @ApiRequest + @JsonSerializable | +path / method / requestType / includeToken / toJson / fromJson / fromJson 注册 | +低 | +
核心优势:
+-
+
- 注解驱动:
@ApiRequest自动生成 mixin,@JsonSerializable自动生成 toJson/fromJson
+ - 自动注册: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。
/// API 请求代码生成器
+class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
+ @override
+ String generateForAnnotatedElement(
+ Element element,
+ ConstantReader annotation,
+ BuildStep buildStep,
+ ) {
+ final className = element.name;
+ final path = annotation.read('path').stringValue;
+ final methodName = _readEnumName(annotation.read('method').objectValue, 'post');
+ final responseType = annotation.read('responseType').typeValue;
+ final responseTypeName = responseType.getDisplayString();
+ final requestTypeName = _readEnumName(annotation.read('requestType').objectValue, 'request');
+
+ // includeToken:默认 login → false,其余 → true
+ final includeTokenReader = annotation.peek('includeToken');
+ final includeToken = (includeTokenReader != null && !includeTokenReader.isNull)
+ ? includeTokenReader.boolValue
+ : requestTypeName != 'login';
+
+ // 生成 mixin,使用侧只需 `with _$XxxApi`
+ return '''
+/// Generated by @ApiRequest for [$className]
+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>? get parameters {
+ registerResponse<$responseTypeName>($responseTypeName.fromJson);
+ return super.parameters;
+ }
+}
+''';
+ }
+}
+
+
+关键设计:parameters getter 在首次请求时自动调用 registerResponse,将 fromJson 注册到全局注册表。无需手动注册,也无需 registerApiResponses() 启动函数。
4.3 build.yaml 配置
+ +文件:packages/networks_sdk/build.yaml
使用 SharedPartBuilder,与 @JsonSerializable 共享同一个 .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 + @JsonSerializable + extends ApiRequestable<T> with _$XxxApi。
发送消息请求(POST):
+// data/remote/send_message_request.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, responseType: SendMessageData)
+@JsonSerializable()
+class SendMessageRequest extends ApiRequestable<SendMessageData>
+ with _$SendMessageRequestApi {
+ @JsonKey(name: 'chat_id')
+ final String chatId;
+ final String content;
+
+ SendMessageRequest({required this.chatId, required this.content});
+ @override
+ Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
+}
+
+
+获取用户资料(GET,靠 token 标识当前用户,无需传参):
+// data/remote/get_profile_request.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, responseType: ProfileData)
+@JsonSerializable()
+class GetProfileRequest extends ApiRequestable<ProfileData>
+ with _$GetProfileRequestApi {
+ GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
+
+ @override
+ Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
+}
+
+
+上传文件请求(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+@JsonSerializable
+ - 零维护:path / method / requestType / includeToken / 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. API 客户端(内部自动挂载 Auth / Retry / Logging 拦截器)
+final apiClientProvider = Provider<ApiClient>((ref) {
+ return ApiClient(config: ref.read(apiConfigProvider));
+});
+
+// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)
+
+/// 3. Repository(注入 domain 接口类型,ViewModel 不感知具体实现)
+final authRepositoryProvider = Provider<AuthRepository>((ref) {
+ final apiConfig = ref.read(apiConfigProvider);
+ return AuthRepositoryImpl(
+ client: ref.read(apiClientProvider), // 直接注入 ApiClient
+ 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
+ → 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 + ApiClient + SocketConfig + SocketClient + SocketManager
+│ └── app_providers.dart # 全局共享状态(themeModeProvider + 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/
+│ │ └── 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
+ │ ├── 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 → ApiClient"] + 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 永远是状态的函数。
+
+ +
第三部分:核心概念(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 ApiClient _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 直接调 ApiClient)
+│ ├── 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 -->|请求| ApiClient[ApiClient
networks_sdk] + Repo -->|缓存| Cache[Cache Manager
data/cache/] + + LocalDS -->|Drift| DB[(Database)] + ApiClient -->|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 ApiClient 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 直接调 ApiClient:无需 RemoteDataSource 中间层 +
- @ApiRequest 注解 + 代码生成:自动实现 path / method / fromJson 注册 +
// 示例:Repository 直接调用 Request
+// data/repositories/message_repository_impl.dart
+class MessageRepositoryImpl implements MessageRepository {
+ final ApiClient _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. 请求远程| ApiClient2[ApiClient] + + ApiClient2 -->|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.json
+feature_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 客户端(executeRequest<T> 唯一入口)
+ │ │ │ └── interceptor/
+ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
+ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
+ │ │ │ └── logging_interceptor.dart # 请求/响应日志
+ │ │ └── socket/
+ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream 输出)
+ │ ├── 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 错误联合类型
+ │ │ ├── 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
+ │ └── 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 # 回调类型定义(OnTokenRefresh 等)
+ ├── 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 内部创建管理 | 构造 ApiClient 传入 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) |
| WebSocket 消息解析 | JSON.decode → Stream 输出 | App 层按 type 过滤 + DTO 解析 |
| Riverpod | 无依赖 | Provider 包装 ApiClient / 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 按需)→ ApiClient,每层职责明确 +
傻瓜式教程:从零开始定义并发送一个接口
+ +前置条件(只需做一次)
+打开一个独立终端窗口,在项目根目录执行以下命令并保持运行,不要关闭:
+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 中调用 ApiClient,转为 Domain Entity
+ +在哪写:lib/data/repositories/auth_repository_impl.dart
做什么:直接调 ApiClient.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 ApiClient _client; // ← 直接注入 ApiClient
+ final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
+
+ AuthRepositoryImpl({
+ required ApiClient client,
+ required void Function(String?) onTokenUpdate,
+ }) : _client = client,
+ _onTokenUpdate = onTokenUpdate;
+
+ @override
+ Future<User> login({
+ required String email,
+ required String password,
+ }) async {
+ // 1. 直接调 ApiClient,构造请求 → 发 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 / ApiClient),业务模块的 Provider 内聚在 Feature 目录下。
// ── features/auth/di/auth_providers.dart ──
+
+// Repository(直接注入 ApiClient + 回调组合多个 SDK 能力)
+final authRepositoryProvider = Provider<AuthRepository>((ref) {
+ final apiConfig = ref.read(apiConfigProvider);
+ return AuthRepositoryImpl(
+ client: ref.read(apiClientProvider), // 直接注入 ApiClient
+ 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(apiClientProvider), // 直接注入 ApiClient
+ );
+});
+
+// 大多数模块只需到 Repository 这一层,ViewModel 直接调 Repository 即可。
+// 如需 UseCase(多步编排、跨模块协调),参考 auth_providers.dart 中的 loginUseCaseProvider。
+
+
+原则:app/di/ 只放 SDK 基础设施(ApiConfig / ApiClient),业务模块的 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) // 直接调 ApiClient
+ → 自动注入 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) // 直接调 ApiClient
+ → AuthInterceptor → Dio.request → RetryInterceptor // 自动处理
+ ← 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 中直接调 ApiClient 就完了:
+ +// 在 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(参考 LingoDot-Flutter)
+ +先向后端获取 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'); },
+ );
+});
+
+/// API 客户端 Provider(全局单例)
+/// 内部自动挂载 AuthInterceptor / RetryInterceptor / LoggingInterceptor
+final apiClientProvider = Provider<ApiClient>((ref) {
+ final config = ref.read(apiConfigProvider);
+ return ApiClient(config: config);
+});
+
+
+DI 装配总览
+ +app/di/ ← 手动装配:SDK 基础设施
+└── network_provider.dart → apiConfigProvider + apiClientProvider
+
+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 / ApiClient),不放业务模块的 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(apiClientProvider), // 直接注入 ApiClient
+ 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'),
+ 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
+
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 ApiClient _client;
+
+ ProfileRepositoryImpl({required ApiClient 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(apiClientProvider), // 直接注入 ApiClient
+ );
+});
+
+// Profile 是简单 CRUD,不需要 UseCase。
+// ViewModel 通过 @riverpod 注解自动生成 Provider,无需额外注册。
+
+
+说明:Profile 属于简单模板,ViewModel 直接调 Repository,无需 UseCase 中间层。app/di/ 只提供 SDK 基础设施(ApiConfig / ApiClient),业务模块的 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/
ApiClient / 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(ApiClient / 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 ApiClient _client; // 直接注入 ApiClient / SocketClient
+
+ 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(apiClientProvider), // 直接注入 ApiClient
+ );
+}
+
+
+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/
ApiClient + + 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. 直接调 ApiClient 请求远程数据 + 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 实现层(直接注入 ApiClient)
+class ChatRepositoryImpl implements ChatRepository {
+ final ApiClient 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();
+ }
+
+ // 直接调 ApiClient 从远程获取
+ 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 各司其职
+