+ +
+

IM App 整体架构设计

+
+

小型 IM 应用完整架构方案

+

Clean Architecture / MVVM / Feature 驱动 / 高内聚低耦合 / 严格分层

+
+
+ + +

Part 0:开发环境配置

+ +

阅读架构之前先把环境跑起来,大约 5 分钟。

+ +

新机器初始化(只需做一次)

+ +

第零步:从 Gitea 拉取代码

+ +

项目托管在内部 Gitea,仅支持 HTTPS 访问(SSH 暂不开放)。需要先在 Gitea 生成个人访问令牌(Personal Access Token)。

+ +

生成 Token 步骤:

+
    +
  1. 登录 Gitea(https://gitea.winwayinfo.com
  2. +
  3. 点击右上角头像 → Settings
  4. +
  5. 左侧菜单选 Applications
  6. +
  7. 在 "Generate New Token" 填写令牌名称(任意)
  8. +
  9. Repository and organization Access 下选择 All
  10. +
  11. Selected token permissions 下所有权限选 Read and Write
  12. +
  13. 点击 Generate Token,复制生成的 token(只显示一次)
  14. +
+ +

克隆仓库:

+
# 将 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 devmain 为主干保护分支
  • +
+
+ +

第一步:安装 Flutter SDK

+ +
    +
  1. 前往 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 } +
    +
  2. +
  3. 解压到固定目录,推荐 ~/flutter: +
    cd ~
    +tar xf ~/Downloads/flutter_macos_arm64_*.tar.xz
    +# 解压后目录为 ~/flutter
    +
    +
  4. +
  5. 写入环境变量: +
    echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.zshrc
    +
    +
  6. +
  7. 验证(在当前终端临时生效): +
    source ~/.zshrc
    +flutter --version
    +dart --version
    +# 确认 Dart 二进制是 arm64(Apple Silicon 机器)
    +file $(which dart)   # 应输出 Mach-O 64-bit executable arm64
    +
    +
  8. +
+ +

第二步:安装 Homebrew + Ruby + CocoaPods(iOS / macOS 必须)

+ +

iOS 和 macOS 平台依赖 CocoaPods 管理原生依赖,CocoaPods 需要 Ruby 3.x,通过 Homebrew 安装。

+ +
+ 必须安装 Ruby 3.3,不能装最新版(4.x)。
+ 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 不够。

+ +
    +
  1. 在 App Store 搜索 Xcode 并安装(约 10 GB)
  2. +
  3. 安装完成后执行: +
    sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    +sudo xcodebuild -runFirstLaunch
    +xcodebuild -version   # 验证
    +
    +
  4. +
+ +
+不装 Xcode 的后果:flutter build ios 报 "Application not configured for iOS",flutter build macos 报 "unable to find utility xcodebuild",两个平台都无法编译。 +
+ +

第三步:配置 IDE

+ +
+

Android Studio

+
    +
  1. 打开 Android Studio
  2. +
  3. 菜单:Settings(Windows/Linux)Preferences(macOS)
  4. +
  5. 导航至 Languages & Frameworks → Flutter
  6. +
  7. Flutter SDK path 填写解压路径,例如 /Users/{你的用户名}/flutter
  8. +
  9. 点击 Apply → OK
  10. +
  11. 重启 Android Studio
  12. +
+
+ +
+

VS Code

+
    +
  1. 在 Extensions 市场搜索并安装 Flutter 插件(Dart 插件会自动一并安装)
  2. +
  3. Cmd+Shift+P 打开命令面板
  4. +
  5. 输入 Flutter: Change SDK,选择 Flutter SDK 解压目录(例如 /Users/{你的用户名}/flutter
  6. +
  7. 重启 VS Code
  8. +
+
+ +
+注意:配置完成后,彻底退出所有终端和 IDE(包括 IDE 内嵌终端),再继续后续步骤。否则新写入的 PATH 不会在已开启的终端中生效。 +
+ +

第三步:运行初始化脚本

+ +

重新打开终端,进入项目根目录执行:

+ +
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 模块识别,不入库)。

+ +
+setup.sh 执行完毕后,再次彻底退出所有终端和 IDE。然后重新打开 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 2flutter 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 的版本号
  • +
+ +
+flowchart TD + App[主应用
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.yamlpath: 依赖引用。

+ +
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_generatorRiverpod Provider 代码生成自动生成 Provider、依赖注入代码大幅减少样板代码,编译期类型安全
freezed不可变数据类生成State 类、copyWith、序列化代码消除手写 State 的繁琐,保证不可变性
json_serializableJSON 序列化/反序列化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);
+  }
+}
+
+ +

开发流程

+ +
+flowchart LR + Write[编写注解代码] --> Watch[build_runner watch] + Watch --> Generate[自动生成 .g.dart] + Generate --> Use[直接使用生成代码] + Use --> Modify[修改源码] + Modify --> Generate + + style Write fill:#e3f2fd,stroke:#0288d1 + style Generate fill:#e8f5e9,stroke:#388e3c + style Use fill:#fff4e6,stroke:#f57c00 +
+ +

代码生成命令

+ +
# 一次性生成
+melos run gen
+
+# 监听模式(开发期间必须常驻)
+melos run gen:watch
+
+ +

代码生成的价值

+ +
    +
  • 大幅减少样板代码:不需要手写 copyWith、equality、hashCode
  • +
  • 编译期类型安全:生成的代码类型完全正确,无运行时错误
  • +
  • 自动化维护:修改字段后自动重新生成,无需手动同步
  • +
  • 统一代码风格:生成的代码风格一致,易于 Code Review
  • +
  • 提升开发效率:专注业务逻辑,不浪费时间在重复代码上
  • +
+ +
+
+

设计理念与目标

+ +

Clean Architecture(整洁架构)

+ +

目的:让业务逻辑与界面设计分离

+ +

方法:通过结构的分层来约束类别间的使用方向

+ +

好处

+
    +
  • 代码更易维护
  • +
  • 代码更易测试
  • +
  • 代码更易扩展
  • +
  • 业务逻辑独立于框架
  • +
  • 业务逻辑独立于UI
  • +
  • 业务逻辑独立于数据库
  • +
+

MVVM(Model-View-ViewModel)状态管理

+ +

演进历史

+ +

在响应式应用中,状态管理经历了以下演进过程:

+ +
+flowchart LR + MVC[MVC
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)
+
    +
  • Controller 职责过重,成为"上帝类"
  • +
  • View 和 Model 耦合,难以测试
  • +
  • View 直接访问 Model,导致业务逻辑泄露到 UI
  • +
  • 难以进行单元测试(需要 UI 环境)
  • +
+
需要更好的关注点分离和可测试性
MVP
(Model-View-Presenter)
+
    +
  • View 和 Presenter 通过接口通信,代码量大
  • +
  • Presenter 需要手动更新 View(调用 view.updateXXX())
  • +
  • 数据变化时需要手动同步到 UI
  • +
  • 样板代码多,维护成本高
  • +
+
需要自动化的数据绑定和响应式更新
MVVM
(Model-View-ViewModel)
+
    +
  • 通过数据绑定实现 UI 自动更新
  • +
  • ViewModel 不持有 View 引用,完全解耦
  • +
  • 状态变化自动反映到 UI,无需手动刷新
  • +
  • 更易测试,ViewModel 可独立测试
  • +
+
响应式编程的最佳实践
+ +
演进的本质:逐步解耦和自动化
+ +
+
    +
  1. MVC → MVP:解决 View 和 Model 的耦合 +
      +
    • 引入 Presenter 作为中介,View 不再直接访问 Model
    • +
    • View 和 Presenter 通过接口通信,提升可测试性
    • +
    • 缺点:手动更新 UI 的样板代码过多
    • +
    +
  2. +
  3. MVP → MVVM:引入数据绑定,实现自动化 +
      +
    • ViewModel 暴露可观察的状态(State)
    • +
    • View 通过数据绑定自动订阅状态变化
    • +
    • 状态更新时,UI 自动刷新,无需手动调用
    • +
    • 优势:代码更简洁,逻辑更清晰,易于维护
    • +
    +
  4. +
+
+ +

前提条件

+ +

状态管理方式高度依赖官方 SDK 的支持与否才可以实现。如果官方 SDK 不支持,某些框架将无法实现。

+ +

实例

+
    +
  • 2012-2019年:Android 开发只支持 MVC 状态管理,无法使用 MVVM
  • +
  • 2020年:Android 官方推出了 BindingView 的 SDK,此后才可以使用 MVVM 做开发
  • +
  • Flutter:从一开始就支持响应式框架,天然适合 MVVM
  • +
+ +

技术栈规定

+ +
+

技术栈升级要求

+

为保证架构的现代化和统一性,必须采用以下技术栈:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
平台UI 框架状态管理说明
iOSSwiftUICombine + ObservationApple 官方声明式 UI + 响应式框架
AndroidJetpack ComposeFlow + LiveDataGoogle 官方声明式 UI + 响应式框架
FlutterWidgetRiverpod跨平台声明式 UI + 现代状态管理
+ +
为什么强制使用这些技术栈?
+ +
+
    +
  • 统一的架构思想:三端都采用声明式 UI + 响应式状态管理,降低学习成本
  • +
  • 官方推荐方案:SwiftUI、Compose 分别是 Apple 和 Google 的官方推荐技术栈
  • +
  • 现代化开发:摒弃 UIKit/XML 等过时技术,拥抱声明式编程范式
  • +
  • 更好的性能:声明式 UI 框架在渲染性能和内存管理上都更优秀
  • +
  • 易于维护:代码更简洁、逻辑更清晰、bug 更少
  • +
  • 团队协作:统一技术栈降低沟通成本,提高开发效率
  • +
+
+ +
学习资源
+ +

iOS - SwiftUI + Combine + Observation:

+ + +

Android - Jetpack Compose + Flow + LiveData:

+ + +

参考学习链接

+ +

深入了解 Flutter 应用架构和 MVVM 模式:

+ + + +

为什么选择 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 混杂
  • +
  • 重复状态offsetoffsetObx 表示同一个东西
  • +
  • 命名混乱:英文、拼音、中文注释混杂
  • +
  • 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 + ObxRiverpod技术优势
刷新颗粒度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 的核心问题:

+
    +
  1. "快速开发"变成"技术债":初期确实快,但 6 个月后代码无法维护
  2. +
  3. "响应式"变成"性能杀手":Obx 嵌套导致过度重建,页面卡顿
  4. +
  5. "灵活"变成"混乱":没有约束,代码风格千差万别
  6. +
  7. "全局管理"变成"依赖噩梦":牵一发动全身,不敢重构
  8. +
  9. "简单上手"变成"难以精通":团队成员写出的代码质量参差不齐
  10. +
+
+ +
+

Riverpod 的技术优势

+

基于 UUTalk 项目的实践教训,我们选择 Riverpod 作为新架构的状态管理方案,因为它从根本上解决了 GetX 的所有问题:

+
    +
  • 编译时安全:不会再有运行时崩溃
  • +
  • 结构化状态:@freezed 强制统一状态结构
  • +
  • 精确重建:依赖追踪精确,性能优异
  • +
  • 职责清晰:ViewModel、Repository、Service 分离明确
  • +
  • 易于测试:Provider 可轻松覆盖和 Mock
  • +
  • 团队规范:代码风格统一,质量可控
  • +
+
+

Riverpod vs Provider 对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性ProviderRiverpod
编译时安全不支持支持
需要 BuildContext需要不需要
代码生成不支持支持
测试友好一般优秀
性能更好
学习曲线平缓稍陡
+ +

MVVM + Riverpod 优势

+ +
    +
  • 清晰的数据流动:数据流向明确,易于追踪
  • +
  • 响应式更新:UI 与数据自动同步,无需手动刷新
  • +
  • 状态管理:统一管理应用状态,避免状态混乱
  • +
  • 可测试性:ViewModel 可独立测试,无需 Widget 环境
  • +
  • 解耦:View 与 Model 完全分离,业务逻辑独立
  • +
  • 类型安全:编译时检查,避免运行时错误
  • +
+

Feature 驱动开发

+ +

以页面为单位

+ +

App 是以页面为导向,设计架构时,必须明确针对平台页面进行开发。每个功能页面独立成一个 Feature。

+ +

完整生命周期

+ +

每个 Feature 包含完整的生命周期:

+ +
+flowchart TD + UI[UI Layer
用户界面] --> 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);
+  }
+}
+
+ +

封装的技术细节

+
    +
  1. 网络可用性检查
  2. +
  3. 请求头自动构建(Token、Content-Type 等)
  4. +
  5. URL 拼接
  6. +
  7. 响应自动解码
  8. +
  9. 错误统一处理和转换
  10. +
+ +
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 + methodtoJson / fromJson
@ApiRequest + @JsonSerializable字段 + 构造函数 + @ApiRequest + @JsonSerializablepath / method / requestType / includeToken / toJson / fromJson / fromJson 注册
+ +

核心优势

+
    +
  • 注解驱动@ApiRequest 自动生成 mixin,@JsonSerializable 自动生成 toJson/fromJson
  • +
  • 自动注册:fromJson 在首次请求时自动注册到全局注册表,无需手动 registerApiResponses()
  • +
  • 一个端点 = 一个文件:Response DTO + Request 放在同一文件,打开即看全貌
  • +
  • 傻瓜式使用:使用者只需关注业务字段和注解配置
  • +
  • 类型安全ApiRequestable<T> 泛型 + responseType 编译期检查
  • +
+ +

跨平台对比

+ + + + + + + + + + + + + + + + + + + + +
平台代码示例简洁度
Swiftstruct 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 分层依赖规则

+ +
+flowchart TD + UI[UI Layer
界面层] + 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 层级。

+ +
+flowchart TD + subgraph App[App Layer] + AppMain[app.dart] + Router[router.dart] + DI[dependency injection] + end + + subgraph Features[Features Layer - 按页面组织] + subgraph Chat[Chat Feature] + ChatUI[UI: chat_page.dart] + ChatVM[Presentation: chat_view_model.dart] + ChatDomain[Domain: usecases + entities] + end + + subgraph ChatList[Chat List Feature] + ChatListUI[UI: chat_list_page.dart] + ChatListVM[Presentation: chat_list_view_model.dart] + ChatListDomain[Domain: usecases] + end + + subgraph Contact[Contact Feature] + ContactUI[UI: contact_page.dart] + ContactVM[Presentation: contact_view_model.dart] + ContactDomain[Domain: usecases] + end + + subgraph Search[Search Feature] + SearchUI[UI: search_page.dart] + SearchVM[Presentation: search_view_model.dart] + SearchDomain[Domain: usecases] + end + + subgraph Call[Call Feature] + CallUI[UI: call_page.dart] + CallVM[Presentation: call_view_model.dart] + CallDomain[Domain: usecases] + end + end + + subgraph Domain[Domain Layer - 共享 Domain] + Repositories[Repositories 接口] + ValueObjects[Value Objects] + end + + subgraph Data[Data Layer] + RepoImpl[Repository 实现] + Models[DTO Models] + end + + subgraph Packages[SDK Packages - Melos 管理] + Network[NetworkSDK] + Storage[StorageSDK] + CipherGuard[CipherGuardSDK
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 驱动的单向数据流。

+ +
+flowchart TD + subgraph Layer5["View 层(MVVM 的 V)"] + direction TB + Pages["ConsumerWidget 页面
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 数据流的精确映射:

+ +
+flowchart LR + subgraph MVVM["MVVM 角色映射"] + direction TB + V["View
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 = newStateref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。

+
+ +
+
+ +

第三部分:核心概念(Core Concepts)

+ + +

Riverpod 核心概念

+ +

在深入了解各层实现之前,先理解 Riverpod 的核心概念和使用方式。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
概念说明使用场景
StateNotifier管理可变状态的类ViewModel 实现
StateNotifierProvider提供 StateNotifier 的 ProviderViewModel Provider
Provider提供不可变对象的 ProviderUseCase、Repository 依赖注入
ConsumerWidget可以监听 Provider 的 WidgetUI 层页面组件
WidgetRef访问 Provider 的引用在 Widget 中读取和监听 Provider
@riverpod代码生成注解自动生成 Provider 代码
@freezed不可变类注解生成 State 类的 copyWith 等方法
autoDispose自动释放 Provider页面销毁时自动清理资源
+ +

Clean Architecture 分层说明

+ +

本架构严格遵循 Clean Architecture 的分层原则,每层都有明确的职责和依赖方向。

+ +

分层职责

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
层级职责依赖方向示例
UI Layer界面展示、用户交互→ PresentationChatPage、MessageItem Widget
Presentation Layer状态管理、UI 逻辑→ DomainChatViewModel、MessageState
Domain Layer业务逻辑、业务规则→ Repository 接口SendMessageUseCase、ChatEntity
Data Layer数据访问、数据源管理→ CoreChatRepository、ChatRepositoryImpl
Core Layer应用级基础设施(Constants/Config/Errors 等)无依赖app_config.dart、error_mapper.dart
SDK Packages可复用技术能力(packages/*_sdk)无依赖NetworkSDK、StorageSDK、L10nSDK
Core UI Layer基础定义、基础组件、业务组合组件→ CoreAppButton、AppDialog、base/colors
+ +

依赖倒置原则(DIP)

+ +

核心思想:高层模块不依赖低层模块,两者都依赖抽象(接口)

+ +
+flowchart TD + VM[ViewModel
表现层] + 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
  • +
+ +
+flowchart LR + UC[UseCase
业务逻辑] + 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 层不是单一层级,而是有明确的分层结构:

+ +
+flowchart TD + UI[UI Layer] --> DesignSystem[Design System
设计系统] + 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、AvatarAtomic Design 原则
L3: Business Components
业务组件
包含业务逻辑的复用组件MessageBubble、ChatItem、ContactCardFeature 级别复用
L4: Pages
页面
完整的页面,组合各种组件ChatPage、ChatListPage、ContactPageFeature 独有
+ +

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/MainAppColors.primary主色
Button/PrimaryAppButton.primary()主按钮
Text/Headline/LargeAppTypography.headlineLarge大标题
Spacing/16AppTokens.spacing1616pt 间距
Radius/MediumAppTokens.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、AndroidCupertino / Material Design触摸交互、竖屏优先
桌面端Windows、macOS、LinuxFluent / 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 平台适配最佳实践

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景推荐方案说明
导航栏PlatformAppBariOS 用 CupertinoNavigationBar,Android 用 AppBar
对话框PlatformDialogiOS 用 CupertinoAlertDialog,Android 用 AlertDialog
按钮PlatformButtoniOS 用 CupertinoButton,Android 用 ElevatedButton
滑动返回自动检测平台iOS 支持侧滑返回,Android 使用返回按钮
右键菜单桌面端显示,移动端长按根据平台调整交互方式
布局ResponsiveLayout手机单栏、平板双栏、桌面三栏
+ +

3.3 Feature UI 组织

+ +

核心理念:UI 层按 Feature 组织,每个页面的 UI 组件都在其对应的 Feature 目录下。

+ +
+flowchart TD + UI[UI Layer 界面层] --> Chat[Chat Feature UI] + UI --> ChatList[Chat List Feature UI] + UI --> Contact[Contact Feature UI] + UI --> Search[Search Feature UI] + UI --> Call[Call Feature UI] + + Chat --> ChatPage[features/chat/view/
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(...) 接收
弹窗 / AlertshowDialog(...)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.gocontext.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 内路由(放进对应 StatefulShellBranchroutes,不加 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):加到对应 StatefulShellBranchroutes 里,不加 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/ 子目录中。

+ +
+flowchart TD + Presentation[Presentation Layer] --> ChatPres[Chat Feature Presentation] + Presentation --> ChatListPres[Chat List Feature Presentation] + Presentation --> ContactPres[Contact Feature Presentation] + Presentation --> SearchPres[Search Feature Presentation] + Presentation --> CallPres[Call Feature Presentation] + + ChatPres --> ChatVM[features/chat/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 设计

+ +
+flowchart TD + UI[UI Layer] -->|用户操作: vm.method()| VM[ViewModel] + VM -->|调用| Repo[Repository] + VM -.复杂场景.-> UC[UseCase(按需)] + UC -.-> Repo + Repo -->|返回 Entity| VM + VM -->|state = newState| State[UI State] + State -->|ref.watch 自动刷新| UI + + style UI fill:#e1f5ff,stroke:#0288d1 + style VM fill:#fff4e6,stroke:#f57c00 + style Repo fill:#e8f5e9,stroke:#388e3c + style UC fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5 + style State fill:#fff4e6,stroke:#f57c00 +
+ +

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 层架构

+ +
+flowchart TD + subgraph FeatureDomain[Feature 专属 Domain] + ChatDomain[features/chat/domain/] + ChatListDomain[features/chat_list/domain/] + ContactDomain[features/contact/domain/] + end + + subgraph GlobalDomain[全局共享 Domain] + RepoInterfaces[domain/repositories/
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 时,遵循单一职责原则:

+ +
+flowchart TD + VM[ViewModel] -->|大多数场景| Repo[Repository Interface
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 实现

+ +
+flowchart TD + Domain[Domain Layer
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 数据流转

+ +
+flowchart LR + UC[UseCase] -->|调用| RepoInterface[Repository Interface
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
baseURLApiConfig.baseURLAppConfig.apiBaseUrl 提供初始值
Token 存储ApiConfig.token(内存)安全存储、持久化
Token 刷新检测过期 → 调 onTokenRefresh提供回调实现
强制登出检测条件 → 调 onForceLogout提供回调(清状态、跳转登录)
错误码定义通用 code != 0 判断定义具体业务码传入
请求定义ApiRequestable 协议 + @ApiRequest 注解各 feature 实现具体 Request
UploaduploadData getter + FormData/Uint8List 支持override uploadData + decodeResponse
WebSocket 连接SocketClient 内部管理(连接/心跳/重连)调 connect/disconnect/send
WebSocket 心跳双层心跳自动管理(底层 ping 5s + 应用层 10s)无需关心
WebSocket 重连指数退避自动重连(1s→2s→4s→8s→16s→30s)无需关心
WebSocket 生命周期提供 onEnterForeground/BackgroundApp 层调用(AppLifecycleListener)
WebSocket 消息解析JSON.decode → Stream 输出App 层按 type 过滤 + DTO 解析
Riverpod无依赖Provider 包装 ApiClient / SocketClient
+ +
命名规范(全链路一致性)
+ +

从 Request 文件到 Domain Entity,所有文件命名必须遵循统一规则,方便区分职责和业务模块。

+ + + + + + + + + + + + + +
层级文件命名类命名示例
接口定义{action}_request.dartRequest: {Action}Request
Response DTO: {Action}Data
login_request.dartLoginRequest + LoginData
持久化 DTOdata/models/{entity}_dto.dart{Entity}Dtouser_dto.dartUserDto
Repository 接口domain/repositories/{module}_repository.dart{Module}Repositoryauth_repository.dartAuthRepository
Repository 实现data/repositories/{module}_repository_impl.dart{Module}RepositoryImplauth_repository_impl.dartAuthRepositoryImpl
Domain Entitydomain/entities/{entity}.dart{Entity}user.dartUser
UseCase(按需)features/{module}/usecases/{action}_usecase.dart{Action}UseCaselogin_usecase.dartLoginUseCase
+ +

关键规则

+
    +
  • 一个端点 = 一个 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所有红线消失

+ +
+

命名规则速查(写之前就能确定引用名)

+ + + + + + + +
你写的类名fromJsontoJsonApi 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 bodyuploadData → FormData / Uint8List
requestTyperequest(默认)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 方法速查表
+ + + + + + + + + + + + +
方法示例接口参数传递方式注解 / 手写
GETGET /user/profile?user_id=123toJson() → URL query parameters@ApiRequest
POSTPOST /auth/logintoJson() → JSON body@ApiRequest
POST(无响应)POST /auth/logouttoJson() → JSON body → 返回 null手写(简单场景)
Upload(FormData)POST /upload/fileuploadData → FormData@ApiRequest + override uploadData
Upload(S3)PUT presigned-urluploadData → Uint8List手写 + override decodeResponse
PUT / PATCHPUT /user/profiletoJson() → JSON body@ApiRequest(同 POST)
DELETEDELETE /message/:idtoJson() → 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)均从共享空间读取。后续只需在多语言后台追加/修改文案,无需发版,立即生效。

+ +

启动流程

+
    +
  1. 拉取远端翻译l10n_loader.dart 在 App 初始化阶段请求远端 URL,下载当前语言的翻译 JSON
  2. +
  3. 写入共享空间:将下载结果持久化到跨层共享目录(Documents / App Group Container),Dart 层和原生层均可读取
  4. +
  5. 加载入口l10n.dart 优先从共享空间读取翻译,命中则使用;未命中则回退到内置兜底文件
  6. +
  7. 原生层同步:Android / iOS 原生代码同样从共享空间读取,与 Dart 层使用同一份翻译数据
  8. +
+ +

兜底机制

+
    +
  • 首次启动(远端尚未下载):使用 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 约束规则

+ +
    +
  1. 位置约束:所有可复用 SDK 在 packages/ 独立 Package,应用级基础设施在 core/foundation/,UI 基础设施在 core/ui/
  2. +
  3. 依赖约束:SDK 之间不能相互依赖(除非明确声明)
  4. +
  5. 职责约束:SDK 只提供纯技术能力,不包含业务逻辑
  6. +
  7. 版本管理:使用 Melos 统一管理版本和依赖
  8. +
  9. 独立性:每个 SDK 可独立测试、发布
  10. +
+ +

环境配置、初始化步骤和 Melos 命令速查表见文档顶部 Part 0:开发环境配置

+ +
+
+ +

扩展性设计

+ +

8.1 新增 Feature

+ +

添加新功能的标准流程:

+ +
    +
  1. features/ 下创建新目录
  2. +
  3. 创建 UI 层页面
  4. +
  5. 创建 Presentation 层 ViewModel
  6. +
  7. 创建 Domain 层 UseCase
  8. +
  9. 在 Domain 层定义 Repository 接口
  10. +
  11. 在 Data 层实现 Repository
  12. +
  13. features/{模块}/di/{模块}_providers.dart 中注册模块 Provider
  14. +
+ +

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(简单模板)

+ +
+flowchart TD + Step1[1. 创建目录结构
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 结构图示

+ +
+flowchart TD + subgraph ProfileFeature[Profile Feature - 垂直切片] + UI[UI Layer
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_riverpodRiverpod 核心库
riverpod_annotationRiverpod 注解,用于代码生成
不可变状态freezed_annotationFreezed 注解,生成不可变类
代码生成build_runnerDart 代码生成工具
riverpod_generatorRiverpod Provider 代码生成
freezedFreezed 代码生成
json_serializableJSON 序列化代码生成
网络dioHTTP 客户端
web_socket_channelWebSocket 通信
存储drift类型安全的响应式数据库(基于 SQLite)
flutter_secure_storage安全存储(加密)
测试mocktailMock 测试工具
+ +

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 命令用途产物
Androidscripts/build_android.shmelos run build:android:apk本地测试 / 内部分发build/app/outputs/flutter-apk/app-release.apk
melos run build:android:aabGoogle Play 上架build/app/outputs/bundle/release/app-release.aab
iOSscripts/build_ios.shmelos run build:iosApp Store / 内部分发build/ios/ipa/im_app.ipa
macOSscripts/build_macos.shmelos run build:macosbuild/macos/Build/Products/Release/im_app.app
Windowsscripts/build_windows.shmelos run build:windowsbuild/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 cleanmelos run clean:deep清除全平台缓存(Flutter / Android Gradle / iOS Pods / macOS Pods / Windows CMake)及所有生成文件,确保 CI 环境干净
Install dependenciesdart pub get在根目录统一解析所有 package 依赖,生成单一 pubspec.lock(Dart pub workspace)
Generate codemelos run gen生成 .g.dart / .freezed.dart*.g.dart 不提交,CI 每次重新生成)
Analyzemelos run analyze对所有 package 执行静态分析,lint 不通过则 PR 不可合并
+ +

触发规则

+
    +
  • pull_requestmain / dev:PR 提交或更新时运行,必须通过才能合并
  • +
  • pushmain / dev:PR 合并后触发(两个分支均开启 branch protection,不允许直接 push)
  • +
+ +

打包策略

+

打包不在自动 CI 中触发,通过 IM 管理后台手动触发打包任务。打包 workflow 单独维护,与 lint/analyze 流水线解耦。

+ +

预留 CI 能力

+ + + + + + + + + + +
能力触发时机状态说明
AI 代码 ReviewPR 提交 / 更新时🔜 预留对每个 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 驱动的架构中流转:

+ +
+sequenceDiagram + participant UI as features/chat/view/
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 流程说明

+ +
    +
  1. 用户操作:用户在 features/chat/view/chat_page.dart 点击发送按钮
  2. +
  3. ViewModel 响应features/chat/presentation/chat_view_model.dart 处理发送逻辑
  4. +
  5. 调用 UseCase:ViewModel 调用 features/chat/usecases/send_message_usecase.dart
  6. +
  7. Repository 接口:UseCase 通过 domain/repositories/message_repository.dart 接口调用
  8. +
  9. Repository 实现data/repositories/message_repository_impl.dart 实现具体逻辑
  10. +
  11. 本地优先:先保存到 data/local/message_local_ds.dart
  12. +
  13. 网络发送:Repository 直接调 SDK(ApiClient / SocketClient)发送
  14. +
  15. 服务器确认:WebSocket 服务器确认接收
  16. +
  17. 状态更新:更新本地数据库中的消息状态
  18. +
  19. 数据返回:层层返回,最终更新 UI
  20. +
+ +

完整代码示例(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 加载会话列表流程

+ +
+sequenceDiagram + participant UI as features/chat_list/view/
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 接口交互:

+ +
+flowchart LR + subgraph ChatFeature[Chat Feature] + ChatUI[UI] + ChatVM[ViewModel] + ChatUC[UseCase] + end + + subgraph ContactFeature[Contact Feature] + ContactUI[UI] + ContactVM[ViewModel] + ContactUC[UseCase] + end + + subgraph SharedDomain[共享 Domain] + MessageRepo[MessageRepository] + ContactRepo[ContactRepository] + end + + subgraph DataLayer[Data Layer] + RepoImpl[Repository 实现] + end + + ChatUC --> MessageRepo + ContactUC --> ContactRepo + MessageRepo -.实现.-> RepoImpl + ContactRepo -.实现.-> RepoImpl + + style ChatFeature fill:#e1f5ff,stroke:#0288d1 + style ContactFeature fill:#e8f5e9,stroke:#388e3c + style SharedDomain fill:#f3e5f5,stroke:#7b1fa2 + style DataLayer fill:#e8f5e9,stroke:#388e3c +
+ +

关键原则

+ +
    +
  • Feature 之间不直接依赖
  • +
  • 通过共享的 Repository 接口通信
  • +
  • Repository 实现在 Data 层统一管理
  • +
  • 保持 Feature 的独立性和可测试性
  • +
+ +

9.5 数据同步策略

+ +

Repository 层负责协调本地和远程数据:

+ +
    +
  • 读取数据:缓存 → 本地数据库 → 远程服务器(三级缓存)
  • +
  • 写入数据:先写入本地,再同步到远程(本地优先)
  • +
  • 冲突解决:使用时间戳或版本号解决冲突
  • +
  • 离线支持:本地数据库支持离线操作,网络恢复后自动同步
  • +
  • 增量同步:只同步变更的数据,减少网络传输
  • +
+ +

9.6 层级依赖总结

+ +
+flowchart TD + subgraph FeatureLayer[Feature Layer - 按页面垂直切片] + direction TB + FeatureView[features/*/view/
页面 + 组件] + 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 架构设计

+ +
+flowchart TD + App[IM App] --> Bridge[Bridge 层] + Bridge --> Capabilities[能力检测] + Bridge --> Protocol[协议转换] + Bridge --> Adapter[平台适配器] + + Adapter --> Enterprise[企业平台] + Adapter --> ThirdParty[第三方应用] + Adapter --> H5[H5 WebView] + Adapter --> Native[原生应用] + + Capabilities --> Check1[网络能力] + Capabilities --> Check2[存储能力] + Capabilities --> Check3[媒体能力] + Capabilities --> Check4[推送能力] + + style Bridge fill:#fff4e6,stroke:#f57c00,stroke-width:3px + style Capabilities fill:#e8f5e9,stroke:#388e3c + style Adapter fill:#e3f2fd,stroke:#2196f3 +
+ +

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 数据获取多层策略

+ +

核心理念:通过内存缓存、热表、冷表、网络请求的多层数据获取策略,优化性能和用户体验。

+ +

数据分层架构

+ +
+flowchart TD + Request[数据请求] --> Memory[L1: 内存缓存] + Memory -->|未命中| Hot[L2: 热表 Hot Table] + Hot -->|未命中| Cold[L3: 冷表 Cold Table] + Cold -->|未命中| Network[L4: 网络请求] + + Network --> Sync[数据同步] + Sync --> UpdateCold[更新冷表] + Sync --> UpdateHot[更新热表] + Sync --> UpdateMemory[更新内存] + + style Memory fill:#e8f5e9,stroke:#388e3c,stroke-width:3px + style Hot fill:#fff4e6,stroke:#f57c00,stroke-width:2px + style Cold fill:#e3f2fd,stroke:#2196f3,stroke-width:2px + style Network fill:#fce4ec,stroke:#c2185b,stroke-width:2px +
+ +

数据层定义

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
层级存储位置数据特点访问速度容量生命周期
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 大量中间层的价值

+ +

核心理念:通过大量的中间层(抽象层、适配层、转换层),实现高度解耦、易于测试、便于替换。

+ +

中间层架构

+ +
+flowchart TD + UI[UI Layer] --> ViewModelAdapter[ViewModel 适配层] + ViewModelAdapter --> UseCase[UseCase 抽象层] + UseCase --> RepositoryInterface[Repository 接口层] + RepositoryInterface --> RepositoryImpl[Repository 实现层] + RepositoryImpl --> NetworkSDK[Network SDK
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
转换层DataDTO ↔ 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 系统能力划分

+ +

核心理念:将系统能力划分为基础能力、业务能力、快速响应机制、部署策略,实现高内聚低耦合。

+ +

能力划分架构

+ +
+flowchart TD + System[IM 系统] --> Basic[基础能力层] + System --> Business[业务能力层] + System --> FastResponse[快速响应层] + System --> Container[容器化部署?] + + Basic --> Network[网络通信] + Basic --> Storage[数据存储] + Basic --> Crypto[加密解密] + Basic --> Media[媒体处理] + + Business --> Chat[聊天功能] + Business --> Contact[联系人] + Business --> Group[群组] + Business --> Call[音视频通话] + + FastResponse --> Cache[智能缓存] + FastResponse --> Preload[预加载] + FastResponse --> Optimize[性能优化] + + Container --> Docker[Docker 容器] + Container --> K8S[Kubernetes] + Container --> CI[CI/CD 流水线] + + style Basic fill:#e8f5e9,stroke:#388e3c,stroke-width:3px + style Business fill:#e3f2fd,stroke:#2196f3,stroke-width:3px + style FastResponse fill:#fff4e6,stroke:#f57c00,stroke-width:3px + style Container fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px +
+ +

基础能力层

+ +

定义:与业务无关的通用技术能力,可复用到任何项目。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
能力说明SDK可复用性
网络通信HTTP/WebSocket/gRPCNetworkSDK完全通用
数据存储Drift/SharedPreferences/SecureStorageStorageSDK完全通用
端对端加密RSA/AES 双层加密 + Native 密钥同步CipherGuardSDK完全通用
媒体处理图片/视频/音频压缩MediaSDK高度通用
音视频通话WebRTC/AgoraRTCSDK较为通用
推送通知FCM/APNs/本地通知NotificationSDK高度通用
推送解密iOS App Group 密钥同步(Notification Extension)CipherGuardSDK高度通用
协议序列化Protocol Buffers/JSONProtocolSDK完全通用
+ +

业务能力层

+ +

定义:IM 领域特定的业务能力,基于基础能力层构建。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
能力说明依赖基础能力可复用性
聊天功能单聊/群聊/消息管理Network + Storage + ProtocolIM 领域高度复用
联系人管理好友/黑名单/通讯录Network + StorageIM 领域高度复用
群组管理创建群/成员管理/权限Network + StorageIM 领域高度复用
音视频通话一对一/多人通话RTC + NetworkIM 领域较为复用
消息推送离线推送/在线推送Notification + NetworkIM 领域高度复用
+ +

快速响应机制

+ +

目标:通过智能缓存、预加载、性能优化,实现极速响应用户操作。

+ +
/// 快速响应管理器
+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 流程

+ +
+flowchart TD + Start[开发完成] --> SelfReview[自我审查] + SelfReview --> Lint[代码检查工具] + Lint --> UnitTest[单元测试] + UnitTest --> PR[提交 PR] + + PR --> AutoCheck[自动检查] + AutoCheck --> PeerReview[同行评审] + + PeerReview -->|不通过| Revise[修改代码] + Revise --> SelfReview + + PeerReview -->|通过| ArchReview[架构师审查] + + ArchReview -->|不通过| Revise + ArchReview -->|通过| Merge[合并到主分支] + + Merge --> Deploy[部署] + + style SelfReview fill:#e8f5e9,stroke:#388e3c + style PeerReview fill:#e3f2fd,stroke:#2196f3 + style ArchReview fill:#fff4e6,stroke:#f57c00 + style Merge fill:#f3e5f5,stroke:#7b1fa2 +
+ +

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 率频繁出现 BugBug 很少显著减少
新功能开发缓慢快速明显提速
技术债务严重很少避免重构
团队效率低(大量救火)高(专注开发)显著提升
代码质量差(难以维护)优(易于维护)可持续发展
+ +

长期收益体现

+ +

初期投入 vs 长期回报

+ +
    +
  • 初期阶段:需要更多时间进行架构设计和基础设施建设
  • +
  • 维护阶段:维护成本大幅降低,团队可以专注新功能开发
  • +
  • 迭代阶段:新功能开发速度显著提升,架构优势逐渐显现
  • +
  • 长期发展:技术债务少,代码库健康,可持续发展
  • +
+ +

收益来源

+ +
    +
  • 维护成本降低:良好的架构设计减少维护工作量
  • +
  • Bug 率降低:严格的分层和测试机制减少 Bug 数量
  • +
  • 开发效率提升:清晰的架构和丰富的基础组件加速开发
  • +
  • 避免重构:提前规划避免后期大规模重构
  • +
+ +

长期收益的关键指标

+ +
+flowchart LR + Architecture[良好架构] --> LowMaintenance[低维护成本] + Architecture --> HighQuality[高代码质量] + Architecture --> FastDevelopment[快速开发] + + LowMaintenance --> MoreTime[更多时间] + HighQuality --> FewerBugs[更少 Bug] + FastDevelopment --> MoreFeatures[更多功能] + + MoreTime --> BusinessValue[业务价值] + FewerBugs --> BusinessValue + MoreFeatures --> BusinessValue + + BusinessValue --> CompetitiveAdvantage[竞争优势] + + style Architecture fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff + style BusinessValue fill:#10b981,stroke:#059669,stroke-width:2px + style CompetitiveAdvantage fill:#f59e0b,stroke:#d97706,stroke-width:2px +
+ +
+

长期收益总结

+
    +
  • 投资回报:初期投入更多时间,长期回报远超初期投入
  • +
  • 维护成本:大幅降低,团队可以专注创新而非救火
  • +
  • Bug 率:显著减少,用户体验持续提升
  • +
  • 开发效率:新功能开发明显提速,快速响应市场
  • +
  • 技术债务:避免大规模重构,保持代码库健康
  • +
  • 团队成长:规范的架构让团队成员快速成长
  • +
  • 竞争优势:更快的迭代速度,更高的产品质量
  • +
+
+ +

5.7 日志与监控系统

+ +

核心理念:通过完善的日志系统和运行监控,实现问题快速定位、性能实时追踪、用户行为分析。

+ +

5.7.1 日志系统设计

+ +

日志分级策略

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
级别用途输出位置保留时间
Debug开发调试信息仅开发环境控制台不保存
Info正常运行信息、关键操作本地文件7 天
Warning潜在问题、异常情况本地文件 + 远程上报30 天
Error错误信息、异常堆栈本地文件 + 远程立即上报90 天
Fatal严重错误、崩溃本地文件 + 远程立即上报 + 告警永久
+ +

日志分类

+ +
+flowchart TD + Logger[日志系统] --> UILog[UI 日志] + Logger --> NetworkLog[网络日志] + Logger --> BusinessLog[业务日志] + Logger --> PerformanceLog[性能日志] + Logger --> SecurityLog[安全日志] + + UILog --> UIAction[用户操作] + UILog --> UIError[UI 错误] + + NetworkLog --> Request[请求记录] + NetworkLog --> Response[响应记录] + NetworkLog --> NetworkError[网络错误] + + BusinessLog --> BizFlow[业务流程] + BusinessLog --> DataChange[数据变更] + + PerformanceLog --> LoadTime[加载时间] + PerformanceLog --> MemUsage[内存占用] + PerformanceLog --> FPS[帧率] + + SecurityLog --> Auth[认证行为] + SecurityLog --> Privacy[隐私操作] + + style Logger fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff + style UILog fill:#e3f2fd,stroke:#2196f3 + style NetworkLog fill:#e8f5e9,stroke:#388e3c + style BusinessLog fill:#fff4e6,stroke:#f57c00 + style PerformanceLog fill:#f3e5f5,stroke:#7b1fa2 + style SecurityLog fill:#fce4ec,stroke:#c2185b +
+ +

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%每次操作
用户行为页面访问路径、功能使用频率-每次操作
+ +

监控系统架构

+ +
+flowchart TD + App[IM App] --> Monitor[监控 SDK] + + Monitor --> Performance[性能监控] + Monitor --> Error[错误监控] + Monitor --> Network[网络监控] + Monitor --> Business[业务监控] + Monitor --> Behavior[行为监控] + + Performance --> Collector[数据采集器] + Error --> Collector + Network --> Collector + Business --> Collector + Behavior --> Collector + + Collector --> LocalCache[本地缓存] + LocalCache --> Uploader[批量上报] + + Uploader --> Backend[监控后端] + Backend --> Analysis[数据分析] + Backend --> Alert[实时告警] + Backend --> Dashboard[监控大盘] + + style Monitor fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff + style Backend fill:#10b981,stroke:#059669,stroke-width:2px + style Alert fill:#ef4444,stroke:#dc2626,stroke-width:2px +
+ +

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 接口解耦
  • +
+ +

核心架构设计

+ +
+flowchart TD + subgraph Principles[设计原则] + FeatureDriven[Feature 驱动] + CleanArch[Clean Architecture] + MVVM[MVVM 状态管理] + end + + subgraph Structure[目录结构] + Features[features/ - 按页面垂直切片] + GlobalDomain[domain/ - 全局接口] + Data[data/ - 统一数据层] + CoreFoundation[core/foundation/ - 应用级基础设施] + L10nSDK[packages/l10n_sdk - 多语言国际化] + CoreUI[core/ui/ - UI 基础设施] + SDKPackages[packages/ - 独立 SDK Packages] + end + + subgraph Benefits[核心优势] + Isolation[功能隔离] + Testable[可测试性] + Scalable[可扩展性] + Maintainable[可维护性] + end + + Principles --> Structure + Structure --> Benefits + + style Principles fill:#e1f5ff,stroke:#0288d1,stroke-width:2px + style Structure fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + style Benefits fill:#e8f5e9,stroke:#388e3c,stroke-width:2px +
+ +

最佳实践

+ +
    +
  1. 严格遵守分层依赖规则:单向向下依赖,禁止反向依赖和跨层调用
  2. +
  3. Feature 垂直切片:每个 Feature 包含 view/presentation/domain 的完整链路
  4. +
  5. 全局 Repository 接口:所有 Repository 接口定义在 domain/repositories/
  6. +
  7. 统一 Data 实现:所有 Repository 实现在 data/repositories/
  8. +
  9. UseCase 单一职责:每个 UseCase 只处理一个业务场景
  10. +
  11. Riverpod 状态管理:使用 StateNotifier 管理 UI 状态,Providers 管理依赖
  12. +
  13. 使用代码生成:利用 riverpod_generator 和 freezed 减少样板代码
  14. +
  15. 使用 Melos 管理依赖:Mono-Repo 保证版本一致性
  16. +
  17. 编写完整测试:单元测试、集成测试、端到端测试
  18. +
  19. UI 层使用 ConsumerWidget:通过 ref.watch 监听状态,ref.read 读取 Provider
  20. +
+ + +

关键原则

+ +
+

Feature 驱动:以页面为单位组织代码,每个 Feature 是完整的垂直切片

+

依赖倒置:高层模块不依赖低层模块,两者都依赖抽象(Repository 接口)

+

单一职责:每个模块只做一件事,UseCase/ViewModel/Repository 各司其职

+
+ +