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. 点击右上角头像 → Settings
  3. 左侧菜单选 Applications
  4. 在 "Generate New Token" 填写令牌名称(任意)
  5. Repository and organization Access 下选择 All
  6. Selected token permissions 下所有权限选 Read and Write
  7. 点击 Generate Token,复制生成的 token(只显示一次)

克隆仓库:

# 将 YOUR_TOKEN 替换为刚才生成的 token,YOUR_USERNAME 替换为你的 Gitea 用户名
git clone https://YOUR_USERNAME:YOUR_TOKEN@gitea.winwayinfo.com/CUS-IM/customer-im-client.git

# 或者先 clone 再配置凭据
git clone https://gitea.winwayinfo.com/CUS-IM/customer-im-client.git
# 输入用户名和 token(token 作为密码)
注意:
  • Token 只在生成时显示一次,务必立即保存
  • 如果 clone 时提示输入密码,输入的是 Token 而不是账号密码
  • 为避免每次 push/pull 都输入,可在克隆地址里内嵌 token:https://user:token@gitea.winwayinfo.com/...
  • 切换到 dev 分支开发:git checkout 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. 解压到固定目录,推荐 ~/flutter
    cd ~
    tar xf ~/Downloads/flutter_macos_arm64_*.tar.xz
    # 解压后目录为 ~/flutter
    
  3. 写入环境变量:
    echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.zshrc
    
  4. 验证(在当前终端临时生效):
    source ~/.zshrc
    flutter --version
    dart --version
    # 确认 Dart 二进制是 arm64(Apple Silicon 机器)
    file $(which dart)   # 应输出 Mach-O 64-bit executable arm64
    

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

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

必须安装 Ruby 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. 安装完成后执行:
    sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    sudo xcodebuild -runFirstLaunch
    xcodebuild -version   # 验证
    
不装 Xcode 的后果:flutter build ios 报 "Application not configured for iOS",flutter build macos 报 "unable to find utility xcodebuild",两个平台都无法编译。

第三步:配置 IDE

Android Studio

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

VS Code

  1. 在 Extensions 市场搜索并安装 Flutter 插件(Dart 插件会自动一并安装)
  2. Cmd+Shift+P 打开命令面板
  3. 输入 Flutter: Change SDK,选择 Flutter SDK 解压目录(例如 /Users/{你的用户名}/flutter
  4. 重启 VS Code
注意:配置完成后,彻底退出所有终端和 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 管理工具,提供:

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 客户端
│           │   │   │   ├── token_refresh_manager.dart   # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用)
│           │   │   │   └── interceptor/
│           │   │   │       ├── auth_interceptor.dart        # Token + 默认 header 注入
│           │   │   │       ├── encryption_interceptor.dart  # 加密拦截器(预留给 cipher_guard_sdk)
│           │   │   │       ├── retry_interceptor.dart       # Token 刷新 + 瞬态错误重试 + 业务错误钩子
│           │   │   │       └── logging_interceptor.dart     # 请求/响应日志
│           │   │   ├── socket/
│           │   │   │   └── socket_client.dart           # WebSocket 长连接(心跳/重连/Stream/加密钩子)
│           │   │   └── networks_sdk_method_channel_datasource.dart  # 统一执行入口
│           │   ├── dto/
│           │   │   ├── api_requestable.dart         # 请求基类 + fromJson 注册表
│           │   │   └── api_response_wrapper.dart    # { code, message/msg, data } 响应包装解析
│           │   └── repositories/
│           │       ├── networks_sdk_repository_impl.dart
│           │       └── networks_messaging_repository_impl.dart
│           ├── domain/
│           │   ├── entities/
│           │   │   ├── api_error.dart               # @freezed HTTP 错误联合类型(7 变体)
│           │   │   ├── encrypted_request.dart       # 加密请求结果数据类
│           │   │   ├── socket_error.dart             # @freezed WebSocket 错误联合类型
│           │   │   ├── socket_connection_state.dart  # 连接状态 enum
│           │   │   ├── http_method.dart              # GET/POST/PUT/DELETE/PATCH
│           │   │   └── api_request_type.dart         # request/login/upload/stream/download
│           │   └── repositories/
│           │       ├── networks_sdk_repository.dart
│           │       └── networks_messaging_repository.dart
│           └── presentation/
│               ├── facade/
│               │   ├── networks_sdk_api.dart         # HTTP 公开 API 接口
│               │   └── networks_messaging_api.dart   # WebSocket 公开 API 接口
│               └── wiring/
│                   ├── api_config.dart               # HTTP 配置(baseURL/Token/回调/加密/重试)
│                   ├── socket_config.dart            # WebSocket 配置(心跳/重连/加密钩子)
│                   ├── network_callbacks.dart        # 回调类型定义(认证/加密/业务错误/下载/WS)
│                   ├── networks_sdk_core.dart
│                   ├── networks_sdk_api_impl.dart
│                   ├── networks_messaging_api_impl.dart
│                   └── networks_sdk_wiring.dart      # 工厂:build() / buildMessagingApi()
│
├── cipher_guard_sdk/                       # 端对端加密(Flutter Plugin)
│   └── lib/
│       ├── cipher_guard_sdk.dart           # barrel file
│       └── src/
│           ├── data/
│           │   ├── constants/
│           │   │   └── method_channel_constants.dart
│           │   ├── datasources/
│           │   │   ├── encryption_flutter_service.dart  # RSA/AES 纯 Dart 加解密(pointycastle + encrypt)
│           │   │   └── encryption_method_channel.dart   # Native 密钥同步(iOS App Group)
│           │   ├── dto/
│           │   │   ├── chat_encryption_key_dto.dart
│           │   │   ├── encrypted_message_dto.dart
│           │   │   ├── method_channel_response.dart
│           │   │   └── rsa_key_pair_dto.dart
│           │   └── repositories/
│           │       └── encryption_repository_impl.dart
│           ├── domain/
│           │   ├── entities/
│           │   │   ├── chat_encryption_key.dart
│           │   │   ├── encrypted_message.dart
│           │   │   ├── rsa_key_pair.dart
│           │   │   └── session_key.dart
│           │   └── repositories/
│           │       └── encryption_repository.dart
│           └── presentation/
│               ├── facade/
│               │   └── cipher_guard_sdk_api.dart    # 公开 API(调用方只依赖这里)
│               └── wiring/
│                   ├── cipher_guard_sdk_api_impl.dart
│                   ├── cipher_guard_sdk_core.dart
│                   └── cipher_guard_sdk_wiring.dart
│
├── storage_sdk/                            # 本地存储(Facade+Wiring,纯基础设施:连接管理 + 通用 CRUD)
│   └── lib/
│       ├── storage_sdk.dart                       # barrel:导出 StorageSdkApi
│       └── src/
│           ├── data/
│           │   ├── local/
│           │   │   └── datasources/
│           │   │       └── database_datasource.dart          # 连接生命周期(openDatabase/closeDatabase)
│           │   └── repositories/
│           │       └── database_repository_impl.dart         # 通用 CRUD 的 Drift 实现
│           ├── domain/
│           │   └── repositories/
│           │       └── database_repository.dart              # 通用 CRUD 接口(泛型,与表无关)
│           └── presentation/
│               ├── facade/
│               │   └── storage_sdk_api.dart                  # 公开接口(生命周期 + 全量 CRUD)
│               └── wiring/
│                   ├── storage_sdk_api_impl.dart             # 委托给 Core
│                   ├── storage_sdk_core.dart                 # 持有 DataSource + Repo
│                   └── storage_sdk_wiring.dart               # build(databaseFactory:) → StorageSdkApi
│
│   # ── 以下 SDK 均遵循相同的 Facade + Wiring 模式,结构参照 cipher_guard_sdk ──
│
├── notification_sdk/                       # 推送通知(Flutter Plugin)
│   └── lib/notification_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── media_sdk/                              # 媒体处理(Flutter Plugin)
│   └── lib/media_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── rtc_sdk/                                # 实时音视频(Flutter Plugin)
│   └── lib/rtc_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── protocol_sdk/                           # 消息协议(Flutter Plugin)
│   └── lib/protocol_sdk.dart + src/{data,domain,presentation}/ (同上模式)
│
├── l10n_sdk/                               # 国际化(Flutter Plugin)
│   └── lib/l10n_sdk.dart + src/{data,domain,presentation}/ (同上模式)

Package 类型说明:

主 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

设计原则:



代码生成工具

本项目大量使用代码生成工具来提升开发效率、减少样板代码,并保证类型安全。

核心代码生成工具

工具 作用 生成内容 优势
riverpod_generator Riverpod Provider 代码生成 自动生成 Provider、依赖注入代码 大幅减少样板代码,编译期类型安全
freezed 不可变数据类生成 State 类、copyWith、序列化代码 消除手写 State 的繁琐,保证不可变性
json_serializable JSON 序列化/反序列化 fromJson/toJson 方法 自动处理 JSON 转换,类型安全
build_runner 代码生成执行器 监听文件变化,自动生成 开发时实时生成,无需手动执行

代码生成示例对比

不使用代码生成(手写样板代码)

// 手写 State 类 - 繁琐且容易出错
class ChatState {
  final List<Message> messages;
  final bool isLoading;
  final String error;

  const ChatState({
    required this.messages,
    required this.isLoading,
    required this.error,
  });

  // 手写 copyWith - 每个字段都要写
  ChatState copyWith({
    List<Message>? messages,
    bool? isLoading,
    String? error,
  }) {
    return ChatState(
      messages: messages ?? this.messages,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }

  // 手写 equality - 容易遗漏字段
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is ChatState &&
      listEquals(other.messages, messages) &&
      other.isLoading == isLoading &&
      other.error == error;
  }

  @override
  int get hashCode => Object.hash(messages, isLoading, error);
}

// 手写 Provider - 样板代码多
final chatViewModelProvider = StateNotifierProvider.autoDispose<
  ChatViewModel, ChatState
>((ref) {
  final sendMessageUseCase = ref.watch(sendMessageUseCaseProvider);
  final loadMessagesUseCase = ref.watch(loadMessagesUseCaseProvider);
  return ChatViewModel(sendMessageUseCase, loadMessagesUseCase);
});

使用代码生成(简洁高效)

// 使用 @freezed - 自动生成 copyWith、equality、toString
part 'chat_state.freezed.dart';

@freezed
class ChatState with _$ChatState {
  const factory ChatState({
    @Default([]) List<Message> messages,
    @Default(false) bool isLoading,
    @Default('') String error,
  }) = _ChatState;
}

// 使用 @riverpod - 自动生成 Provider
part 'chat_view_model.g.dart';

@riverpod
class ChatViewModel extends _$ChatViewModel {
  @override
  ChatState build() => const ChatState();

  Future<void> sendMessage(String content) async {
    state = state.copyWith(isLoading: true);
    await ref.read(sendMessageUseCaseProvider)(content);
    state = state.copyWith(isLoading: false);
  }
}

开发流程

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

代码生成的价值



设计理念与目标

Clean Architecture(整洁架构)

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

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

好处

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. MVP → MVVM:引入数据绑定,实现自动化
    • ViewModel 暴露可观察的状态(State)
    • View 通过数据绑定自动订阅状态变化
    • 状态更新时,UI 自动刷新,无需手动调用
    • 优势:代码更简洁,逻辑更清晰,易于维护

前提条件

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

实例

技术栈规定

技术栈升级要求

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

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

iOS - SwiftUI + Combine + Observation:

Android - Jetpack Compose + Flow + LiveData:

参考学习链接

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

为什么选择 Riverpod?

本项目使用 Riverpod 作为状态管理方案,基于以下核心技术优势和实践教训:

1. 性能优化机制
刷新颗粒度(Rebuild Granularity)

GetX + Obx 的问题:

实际案例:

// 整个 Container 都会重建
Obx(() => Container(
  height: controller.height.value,  // 高度变化
  child: Column(
    children: [
      Text(controller.title.value),     // title 变化也会重建整个 Container
      Text(controller.content.value),   // content 变化也会重建整个 Container
    ],
  ),
))

Riverpod 的精细化刷新优化:

优化技术 作用 使用场景
select 精确订阅状态的某个字段 只关心状态中的部分数据,避免整个对象变化导致重建
Consumer 局部重建 Widget 树的一部分 只重建需要响应状态变化的最小 Widget 子树
RepaintBoundary 隔离重绘边界 阻止父级重绘影响子级,或子级重绘影响父级
autoDispose 自动释放不再使用的 Provider 页面销毁时自动清理资源,避免内存泄漏
family 为不同参数创建独立 Provider 实例 列表中每个 Item 有独立状态,互不影响
代码示例
// 1. select:只订阅 title 字段,content 变化时不重建
final title = ref.watch(chatViewModelProvider.select((s) => s.title));

// 2. Consumer:只重建 Consumer 包裹的部分
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('静态内容,永远不重建'),
      Consumer(
        builder: (context, ref, child) {
          final count = ref.watch(counterProvider);
          return Text('动态内容: $count'); // 只有这里会重建
        },
      ),
      Text('静态内容,永远不重建'),
    ],
  );
}

// 3. RepaintBoundary:隔离复杂动画,避免影响其他 Widget
RepaintBoundary(
  child: ComplexAnimationWidget(), // 动画重绘不会影响外部
)

// 4. autoDispose:页面销毁时自动释放资源
final userProvider = StateNotifierProvider.autoDispose<UserViewModel, UserState>(
  (ref) => UserViewModel(),
);

// 5. family:列表中每个 Item 有独立状态
final messageProvider = Provider.family<Message, int>((ref, messageId) {
  return ref.watch(chatRepositoryProvider).getMessageById(messageId);
});

性能对比:

场景 不使用优化 使用优化 提升
聊天列表滚动 所有 Item 重建 只重建可见 Item 性能显著提升
状态局部更新 整个页面重建 只重建 Consumer 部分 大幅减少 rebuild
动画播放 影响整个 Widget 树 RepaintBoundary 隔离 流畅度明显提升
列表 Item 状态 共享状态,互相影响 family 独立状态 避免无关重建

性能提升效果:

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)),
      ],
    );
  }
}

数据流混乱导致:

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),
      ],
    );
  }
}

优势:

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,类型不匹配!

运行时错误案例:

Riverpod 的编译期保证

保证1:依赖必须存在

@riverpod
class ChatViewModel extends _$ChatViewModel {
  @override
  ChatState build() {
    // 编译期就知道依赖了什么
    final chatRepo = ref.watch(chatRepositoryProvider);  // 如果 Provider 不存在,编译报错
    return const ChatState();
  }
}

保证2:类型完全正确

// 类型由编译器推断,完全正确
final state = ref.watch(chatViewModelProvider);  // state 类型是 ChatState
final viewModel = ref.read(chatViewModelProvider.notifier);  // viewModel 类型是 ChatViewModel

保证3:依赖图可视化

// Riverpod DevTools 可以看到完整的依赖图
ChatViewModel
  ├─ chatRepositoryProvider
  │   ├─ networkSdkApiProvider
  │   └─ messageLocalDataSourceProvider
  └─ sendMessageUseCaseProvider
      └─ chatRepositoryProvider

编译期保证的价值:

GetX + Obx vs Riverpod:实践教训

典型问题示例

以下内容展示了 GetX + Obx 在大型项目中暴露的典型问题,作为选型参考。

1. 状态管理混乱

GetX + Obx 问题代码示例(chat_list_controller.dart):

class ChatListController extends GetxController {
  var lastClickedSpecialChatId = (-1).obs;
  final chatList = <Chat>[].obs;
  final companyChatList = <Chat>[].obs;
  final lockedChatList = <Chat>[].obs;
  final RxBool isNavigating = false.obs;
  final isInitializing = true.obs;
  ValueNotifier<bool> isInitializingValue = ValueNotifier(true);  // 混用!
  final isShowSkeleton = false.obs;
  ValueNotifier<double> offset = ValueNotifier(0);  // 混用!
  RxDouble offsetObx = 0.0.obs;  // 重复状态!
  RxDouble miniAppIconOpacityWhenShowingMiniApp = 1.0.obs;
  RxDouble miniAppIconOpacityWhenShowingChatView = 0.01.obs;
  RxDouble miniAppIconScale = 0.5.obs;
  RxBool isShowingApplet = false.obs;
  RxBool isMiniAppletDraggingIcon = false.obs;
  RxBool isBeingHovered = false.obs;
  RxBool isShowDeleteBar = true.obs;
  // ... 还有 50+ 个响应式变量!
}

问题分析:

  • 状态零散:50+ 个独立的 .obs 变量,没有统一结构
  • 混合使用RxBool, RxDouble, ValueNotifier 混杂
  • 重复状态offsetoffsetObx 表示同一个东西
  • 命名混乱:英文、拼音、中文注释混杂
  • UI 状态污染:动画、透明度、偏移量都混在 Controller 里
2. 过度嵌套和性能问题

Obx 嵌套地狱示例(home_view.dart):

body: Obx(() => AnimatedPadding(
  child: GetBuilder(
    builder: (_) => Obx(
      () => Stack(
        children: [
          Obx(
            () => Container(
              child: Obx(
                () => Opacity(
                  opacity: controller.opacity.value,
                  child: Obx(
                    () => AnimatedContainer(...),  // 第6层 Obx!
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    ),
  ),
)),

问题分析:

  • 嵌套地狱:单个文件 12 个 Obx,最深 6 层嵌套
  • 过度重建:每个 Obx 独立监听,导致大量不必要的 rebuild
  • Obx + GetBuilder 混用:逻辑混乱,性能更差
  • 难以调试:无法追踪哪个状态导致了 rebuild
3. Controller 过于臃肿

巨型 Controller 反模式示例:

// chat_list_controller.dart
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:bot_toast/bot_toast.dart';
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
// ... 共 77 个 import!

class ChatListController extends GetxController
    with GetTickerProviderStateMixin,
         WidgetsBindingObserver,
         LockedOverlayMixin {  // 多个 Mixin 混杂

  // UI 逻辑
  void handleScroll() { ... }
  void animateOpacity() { ... }

  // 业务逻辑
  Future<void> loadChats() { ... }
  Future<void> sendMessage() { ... }

  // 动画逻辑
  void startAnimation() { ... }
  late AnimationController animController;

  // 路由逻辑
  void navigateToChat() { ... }

  // ... 几千行代码全在一个文件!
}

问题分析:

  • 77 个 import:依赖关系复杂,难以维护
  • 职责不清:UI、业务、动画、路由全混在一起
  • 文件巨大:单个 Controller 文件几千行
  • 难以复用:逻辑耦合,无法独立使用
4. 没有编译时安全

GetX 运行时陷阱:

// 通过字符串查找 Controller,运行时才知道对错
Get.find<ChatListController>();
Get.put(ChatListController());  // 全局单例,容易冲突

// 依赖注入不明确,不知道依赖了什么
class ChatListController extends GetxController {
  void loadData() {
    // 直接访问全局 Manager,依赖不透明
    objectMgr.chatMgr.getChats();
    objectMgr.groupMgr.getGroups();
    // 不知道这个 Controller 到底依赖了什么
  }
}

问题分析:

  • 运行时错误:编译通过,运行时才崩溃
  • 全局污染Get.find 全局查找,容易命名冲突
  • 依赖不明:不知道 Controller 依赖了哪些服务
  • 难以重构:改一个地方,不知道影响范围
5. 难以测试

GetX 测试困境:

// 测试时必须初始化整个 GetX 框架
testWidgets('test chat list', (tester) async {
  // 必须先初始化所有依赖
  Get.put(ObjectMgr());
  Get.put(ChatMgr());
  Get.put(GroupMgr());
  // ... 几十个依赖

  final controller = Get.put(ChatListController());

  // 无法 mock,因为 Controller 直接依赖全局对象
  // objectMgr.chatMgr 是全局单例,无法替换
});

问题分析:

  • 测试困难:需要初始化整个 GetX 生态
  • 无法 Mock:依赖全局对象,无法注入 mock
  • 测试耦合:测试依赖 GetX 框架
  • 覆盖率低:太难测,团队放弃测试
GetX + Obx vs Riverpod 实际对比
技术维度 GetX + Obx Riverpod 技术优势
刷新颗粒度 Obx 包裹的整个 Widget
无法精确控制范围
精确依赖追踪
支持 select 细粒度订阅
大幅减少不必要 rebuild
性能基准 状态更新较慢
内存占用较高
状态更新快速
内存占用较低
性能明显提升
内存显著减少
数据流 双向绑定
状态可在任意位置修改
单向数据流
状态只能通过 ViewModel 修改
数据流清晰可追踪
便于状态历史回溯
编译期安全 运行时查找 Controller
类型错误运行时才发现
编译期类型检查
依赖不存在立即报错
大幅减少 Bug
提升开发效率
代码生成 不支持 riverpod_generator
freezed 全面支持
大幅减少样板代码
显著提升开发效率
依赖管理 全局 Get.put/find
依赖关系不透明
Provider 依赖图
编译期验证依赖
依赖关系可视化
重构安全有保障
测试支持 需要初始化 GetX 框架
无法 Mock 全局依赖
Provider 可轻松覆盖
纯函数易于测试
测试更容易
运行速度更快
DevTools 基础日志 完整依赖图
状态历史回溯
性能分析
Debug 效率显著提升
真实案例对比

GetX + Obx

状态混乱:

class ChatListController extends GetxController {
  final chatList = <Chat>[].obs;
  final isLoading = false.obs;
  ValueNotifier<double> offset = ValueNotifier(0);  // 混用!
  RxDouble offsetObx = 0.0.obs;  // 重复!
  RxBool isShowingApplet = false.obs;
  RxBool isMiniAppletDraggingIcon = false.obs;
  // ... 50+ 个零散变量

  void loadChats() {
    objectMgr.chatMgr.getChats();  // 全局依赖,不可测
  }
}

UI 嵌套地狱:

Obx(() => GetBuilder(
  builder: (_) => Obx(
    () => Obx(
      () => Obx(() => ...), // 6 层嵌套!
    ),
  ),
))

Riverpod(新架构)

状态清晰:

@freezed
class ChatListState with _$ChatListState {
  const factory ChatListState({
    @Default([]) List<Chat> chats,
    @Default(false) bool isLoading,
  }) = _ChatListState;
}

class ChatListViewModel extends StateNotifier<ChatListState> {
  ChatListViewModel(this._chatRepository)
      : super(const ChatListState());

  final ChatRepository _chatRepository;  // 依赖明确

  Future<void> loadChats() async {
    state = state.copyWith(isLoading: true);
    final chats = await _chatRepository.getChats();
    state = state.copyWith(chats: chats, isLoading: false);
  }
}

UI 清晰:

class ChatListPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(chatListViewModelProvider);
    return state.isLoading
      ? LoadingWidget()
      : ChatListView(chats: state.chats);
  }
}
GetX + Obx 痛点总结

从实践经验中,我们总结出 GetX + Obx 的核心问题:

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

Riverpod 的技术优势

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

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

Riverpod vs Provider 对比

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

MVVM + Riverpod 优势

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

高内聚低耦合

模块设计哲学

核心设计原则

"实现层高度封装,使用侧傻瓜式"

这是本架构所有模块设计遵循的核心哲学:

  • 实现层高度封装:将复杂的技术细节、错误处理、类型转换等全部封装在底层
  • 使用侧傻瓜式:上层使用者只需关注业务逻辑,无需了解底层实现细节
  • 按需使用:提供合理的默认值和可选参数,使用者可以按需定制

网络层设计示例

以网络层为例,展示如何实现"实现层高度封装,使用侧傻瓜式"的设计原则。

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();
  }
}

核心优势

2. 统一执行入口 - executeRequest

设计思想:提供唯一的请求执行入口,自动处理所有技术细节

/// 执行 API 请求 - 唯一的请求入口
Future<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
  final client = ref.read(networkSdkApiProvider);
  final config = ref.read(apiConfigProvider);

  // 1. 检查网络连接
  if (!networkManager.isNetworkAvailable) {
    throw const APIError.noNetworkConnection();
  }

  try {
    // 2. 根据请求类型构建 header
    final headers = configNotifier.defaultHeaders(
      customHeaders: request.customHeaders,
      includeToken: request.requestType != APIRequestType.login,
    );

    // 3. 执行请求
    final response = await dio.request(
      '${config.baseURL}${request.path}',
      data: request.parameters,
      options: Options(method: request.method.value, headers: headers),
    );

    // 4. 自动解码响应
    return request.decodeResponse(response);
  } on DioException catch (e) {
    // 5. 统一错误处理
    throw _handleDioError(e);
  }
}

封装的技术细节

  1. 网络可用性检查
  2. 请求头自动构建(Token、Content-Type 等)
  3. URL 拼接
  4. 响应自动解码
  5. 错误统一处理和转换
3. 自动响应解码 - 注册机制

设计思想:通过注册机制,自动查找 fromJson 函数,实现响应的自动解码

/// 响应类型注册表
final _fromJsonRegistry = <Type, Function>{};

/// 注册响应类型 - 一次注册,全局可用
T Function(Map<String, dynamic>)? registerResponse<T>(
  T Function(Map<String, dynamic>) fromJson,
) {
  _fromJsonRegistry[T] = fromJson;
  return fromJson;
}

/// 自动解码扩展 - 使用侧无需关心
extension APIRequestableExtension<T> on APIRequestable<T> {
  T? decodeResponse(Response response) {
    final data = response.data as Map<String, dynamic>;

    // 从注册表查找 fromJson 函数
    final fromJsonFunc = _fromJsonRegistry[T] as T Function(Map<String, dynamic>)?;

    if (fromJsonFunc == null) {
      throw StateError('fromJson not registered for type $T');
    }

    // 自动解码 APIResponseWrapper
    final wrapper = APIResponseWrapper<T>.fromJson(
      data,
      (json) => fromJsonFunc(json as Map<String, dynamic>),
    );

    // 检查业务错误码
    if (wrapper.code != 0) {
      throw APIError.apiError(code: wrapper.code, message: wrapper.message);
    }

    return wrapper.data;
  }
}

使用示例

一个端点 = 一个文件(data/remote/login_request.dart),Response DTO + Request 放在同一文件中。

import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';

part 'login_request.g.dart';

// ── Response DTO ──

@JsonSerializable()
class LoginData {
  final String token;
  @JsonKey(name: 'user_id')
  final String userId;
  final String email;

  const LoginData({required this.token, required this.userId, required this.email});
  factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
  Map<String, dynamic> toJson() => _$LoginDataToJson(this);

  User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
}

// ── Request(零样板:只需 @ApiRequest,无需 @JsonSerializable / fromJson / toJson)──
// @ApiRequest 自动生成 path / method / requestType / includeToken / toJson / fromJson 注册

@ApiRequest(
  path: ApiPaths.authLogin,                // 路径统一在 core/foundation/api_paths.dart 管理
  method: HttpMethod.post,
  responseType: LoginData,
  requestType: ApiRequestType.login,
)
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
  final String email;
  final String password;

  LoginRequest({required this.email, required this.password});
  // 完毕!toJson 由 mixin 从类字段自动生成,fromJson 不需要(Request 永远手动构造)
}
// 使用 - 超级简单!
final loginData = await apiClient.executeRequest(
  LoginRequest(email: 'user@example.com', password: '123456'),
);
final user = loginData?.toEntity(); // DTO → Domain Entity
设计思想对比
方案 需要定义的内容 自动生成 维护成本
手动方式 字段 + 构造函数 + extends + path + method + toJson + parameters + registerResponse
JsonSerializable 字段 + 构造函数 + extends + path + method toJson / fromJson
@ApiRequest(当前方案) 字段 + 构造函数 + @ApiRequest path / method / requestType / includeToken / toJson / fromJson 注册 极低

核心优势

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

跨平台对比

平台 代码示例 简洁度
Swift struct LoginRequest: APIRequestable { typealias Response = LoginData ... } 协议直接实现,最简洁
Dart @ApiRequest(...) class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi { ... } 注解 + 代码生成,接近 Swift 体验

Dart 通过注解 + 代码生成弥补语言层面没有协议默认实现的不足,达到接近 Swift 的简洁度。

4. 注解定义与代码生成器

设计思想:通过注解 + 代码生成器,自动生成所有技术代码

4.1 注解定义

文件:packages/networks_sdk/lib/src/annotations/api_request.dart

/// API 请求注解 - 标记一个类为 API 请求
///
/// 代码生成器会自动生成 mixin `_$Api`,提供:
/// - path / method / requestType / includeToken 协议实现
/// - 自动注册 responseType 的 fromJson(在 parameters getter 中触发)
class ApiRequest {
  /// API 路径(如 '/auth/login')
  final String path;

  /// HTTP 方法(默认 POST)
  final HttpMethod method;

  /// 响应类型(用于泛型绑定 + 自动注册 fromJson)
  final Type responseType;

  /// 请求类型(决定 header 处理方式)
  final ApiRequestType requestType;

  /// 是否携带 Token(默认根据 requestType 推断:login → false,其余 → true)
  final bool? includeToken;

  /// 自定义请求头
  final Map<String, String>? customHeaders;

  const ApiRequest({
    required this.path,
    this.method = HttpMethod.post,
    required this.responseType,
    this.requestType = ApiRequestType.request,
    this.includeToken,
    this.customHeaders,
  });
}
4.2 代码生成器核心逻辑

文件:packages/networks_sdk/lib/src/generator/api_request_generator.dart

生成 mixin(非 extension),因为 mixin 可以 override 基类方法、调用 super,并在 parameters getter 中自动注册 fromJson。

toJson 生成机制:生成器读取类的声明字段(非继承),直接在 mixin 中生成 Map 字面量。不依赖 @JsonSerializable,避免了继承属性被序列化导致的递归问题。支持 @JsonKey(name: '...') 字段重命名和 @JsonKey(includeToJson: false) 跳过字段。

/// API 请求代码生成器
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
  @override
  String generateForAnnotatedElement(element, annotation, buildStep) {
    final className = element.name;
    // ... 读取 path / method / responseType / requestType / includeToken ...

    // 从类的声明字段生成 toJson(),只序列化自身字段,不含继承属性
    final toJsonBody = _buildToJsonBody(element, className);

    return '''
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
  @override String get path => '$path';
  @override HttpMethod get method => HttpMethod.$methodName;
  @override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
  @override bool get includeToken => $includeToken;
  @override
  Map<String, dynamic> toJson() => $toJsonBody;
  @override
  Map<String, dynamic>? get parameters {
    registerResponse<$responseTypeName>($responseTypeName.fromJson);
    return super.parameters;
  }
}
''';
  }

  /// 读取类的声明字段,生成 Map 字面量
  /// 支持 @JsonKey(name: '...') 重命名
  String _buildToJsonBody(ClassElement element, String className) {
    final fields = element.fields.where((f) => !f.isStatic && !f.isSynthetic);
    // => {'email': (this as LoginRequest).email, 'password': ...}
  }
}

关键设计

4.3 build.yaml 配置

文件:packages/networks_sdk/build.yaml

使用 SharedPartBuilder,与 @JsonSerializable(Response DTO 用)共享同一个 .g.dart 文件,无需额外 part 指令。

builders:
  api_request:
    import: "package:networks_sdk/src/generator/builder.dart"
    builder_factories: ["apiRequestBuilder"]
    build_extensions: {".dart": [".api_request.g.part"]}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]
4.4 运行命令

⚠️ 强制要求:开发期间必须常驻 watch 模式

# 在项目根目录打开一个独立终端窗口,执行(整个开发期间只需一次):
melos run gen:watch

启动后,每次保存 .dart 文件都会自动重新生成 .g.dart
手写代码时 IDE 报红是正常的,保存后红线自动消失。

# 仅在 watch 出问题时使用:全量重新生成
melos run gen
4.5 更多使用示例

所有示例遵循同一模式:@ApiRequest + extends ApiRequestable<T> with _$XxxApi。Request 类无需 @JsonSerializable

发送消息请求(POST + @JsonKey 字段重命名):

// data/remote/send_message_request.dart

// ── Response DTO(仍用 @JsonSerializable)──
@JsonSerializable()
class SendMessageData {
  @JsonKey(name: 'message_id')
  final String messageId;
  final int timestamp;

  const SendMessageData({required this.messageId, required this.timestamp});
  factory SendMessageData.fromJson(Map<String, dynamic> json) =>
      _$SendMessageDataFromJson(json);
}

// ── Request(零样板)──
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
class SendMessageRequest extends ApiRequestable<SendMessageData>
    with _$SendMessageRequestApi {
  @JsonKey(name: 'chat_id')  // 生成器会读取,JSON 键名为 'chat_id'
  final String chatId;
  final String content;

  SendMessageRequest({required this.chatId, required this.content});
  // toJson 自动生成:{'chat_id': chatId, 'content': content}
}

获取用户资料(GET,靠 token 标识当前用户,无需传参):

// data/remote/get_profile_request.dart

@JsonSerializable(createToJson: false)  // 只需反序列化
class ProfileData {
  @JsonKey(name: 'user_id')
  final String userId;
  final String email;
  final String? nickname;
  final String? avatar;

  const ProfileData({required this.userId, required this.email, this.nickname, this.avatar});
  factory ProfileData.fromJson(Map<String, dynamic> json) =>
      _$ProfileDataFromJson(json);

  User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
}

@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
class GetProfileRequest extends ApiRequestable<ProfileData>
    with _$GetProfileRequestApi {
  GetProfileRequest();  // 无参数 — toJson 自动生成空 map
}

上传文件请求(FormData multipart):

// data/remote/upload_file_request.dart

@JsonSerializable()
class UploadResult {
  final String url;
  @JsonKey(name: 'file_id')
  final String fileId;
  const UploadResult({required this.url, required this.fileId});
  factory UploadResult.fromJson(Map<String, dynamic> json) =>
      _$UploadResultFromJson(json);
}

@ApiRequest(
  path: ApiPaths.uploadFile,
  method: HttpMethod.post,
  responseType: UploadResult,
  requestType: ApiRequestType.upload,
)
class UploadFileRequest extends ApiRequestable<UploadResult>
    with _$UploadFileRequestApi {
  final String filePath;
  final String? fileName;

  UploadFileRequest({required this.filePath, this.fileName});

  @override
  Map<String, dynamic> toJson() => {};  // upload 不走 toJson

  @override
  Object? get uploadData => FormData.fromMap({
    'file': MultipartFile.fromFileSync(filePath, filename: fileName),
  });
}

核心价值

  • 极简使用:字段 + 构造函数 + @ApiRequest(Request 无需 @JsonSerializable、无需 fromJson、无需手写 toJson
  • 零维护:path / method / requestType / includeToken / toJson / fromJson 注册 全部自动生成
  • 类型安全:泛型 ApiRequestable<T> + responseType 编译期检查
  • 一个端点 = 一个文件:Response DTO + Request 放在同一文件,打开即看全貌
5. Riverpod 集成 - 依赖注入

设计思想:Network SDK 本身零 Flutter / 零 Riverpod 依赖。App 层通过 Provider 包装,实现全局单例 + 依赖注入。

DI 装配

// ── app/di/network_provider.dart ── (SDK 基础设施,全局唯一)

/// 1. API 配置(baseURL 来自 config.json → --dart-define-from-file)
final apiConfigProvider = Provider<ApiConfig>((ref) {
  return ApiConfig(
    baseURL: AppConfig.apiBaseUrl,
    platformHeaders: {'Platform': 'Android', 'client-version': '1.0.0'},
    tokenExpiredCodes: {30002, 30003, 30124},
    forceLogoutCodes: {30125},
    onForceLogout: () { /* 清除登录态,跳转登录页 */ },
    onTokenRefresh: () async { /* 刷新 token */ return null; },
    onLog: (message, {tag}) { print('[$tag] $message'); },
  );
});

/// 2. Networks SDK API Provider(全局单例,Facade 接口)
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
  final config = ref.read(apiConfigProvider);
  return NetworksSdkWiring.build(config: config);
});

// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)

/// 3. Repository(注入 Facade 接口类型,ViewModel 不感知具体实现)
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  final apiConfig = ref.read(apiConfigProvider);
  return AuthRepositoryImpl(
    client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
    onTokenUpdate: (token) {
      apiConfig.updateToken(token);               // 内存(networks_sdk)
      // secureStorage.saveToken(token);          // 持久化(storage_sdk,待接入)
    },
  );
});

/// 4. UseCase(按需)
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
  return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});

ViewModel 中使用

@riverpod
class LoginViewModel extends _$LoginViewModel {
  @override
  LoginState build() => const LoginState();

  Future<void> login(String email, String password) async {
    state = state.copyWith(isLoading: true);

    try {
      // UseCase 封装格式校验 + 业务编排,ViewModel 只需一行
      final user = await ref.read(loginUseCaseProvider).execute(
        email: email, password: password);

      state = state.copyWith(user: user, isLoading: false);
    } on FormatException catch (e) {
      // 格式校验失败(UseCase 层抛出)
      state = state.copyWith(error: e.message, isLoading: false);
    } on ApiError catch (e) {
      // 统一错误处理 - Freezed union type
      state = state.copyWith(error: e.displayMessage, isLoading: false);
    }
  }
}

完整装配链路

View: ref.watch(loginViewModelProvider)
  → ViewModel: ref.read(loginUseCaseProvider).execute(...)
    → LoginUseCase: 格式校验(邮箱 + 密码)
    → LoginUseCase: authRepository.login(...)
      → Repository: _client.executeRequest(LoginRequest(...))
        → ApiClient.executeRequest()             ← networks_sdk 内部
          → AuthInterceptor                      ← 注入 token + headers
          → EncryptionInterceptor                ← 加密请求体(预留)
          → Dio.request(baseURL + path, data)    ← 实际 HTTP 请求
          → RetryInterceptor                     ← token 过期自动刷新重试 + 业务错误钩子
          → LoggingInterceptor                   ← 请求/响应日志
        ← request.decodeResponse(response)       ← 自动解码
          ← ApiResponseWrapper.fromJson          ← 拆 { code, msg, data }
          ← fromJsonRegistry[LoginData]          ← 查注册表
          ← LoginData.fromJson(data)             ← 反序列化
      ← LoginData(DTO)
      → onTokenUpdate(token)                     ← 回调写入 Token(内存 + 持久化)
      ← loginData.toEntity() → User              ← DTO → Domain Entity
    ← User
  ← state.copyWith(user: user)                   ← 更新状态
View: ref.watch → 自动 rebuild                    ← UI 刷新

设计哲学在本架构中的体现

层级 实现层(高度封装) 使用侧(傻瓜式)
网络层 executeRequest 封装所有细节
自动 header、解码、错误处理
定义 Request 类
只需 3 个字段:path、method、parameters
数据层 Repository 封装数据源切换
自动缓存、错误转换
调用 Repository 方法
返回 Domain 模型,无需关心数据来源
业务层 UseCase 封装业务逻辑
单一职责,可组合
ViewModel 调用 UseCase
专注状态管理,不关心业务细节
UI层 ViewModel 提供响应式状态
自动重建、错误处理
Widget 监听 Provider
只需 ref.watch,无需手动管理状态

核心优势

设计哲学总结

本架构中的每一个模块都遵循"实现层高度封装,使用侧傻瓜式"的原则:

  • 底层模块负责技术复杂性,提供简洁的 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

依赖方向:单向向下,严格禁止反向依赖和跨层调用

严格规则

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 + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
│       ├── db_provider.dart                 # StorageSdkApi(注入 AppDatabase factory)
│       └── app_providers.dart               # AppInitializer + ThemeModeNotifier + AuthNotifier
│
├── features/                                # 功能模块(垂直切片):Feature 间禁止直接 import
│   │
│   ├── app_tab/                             # Tab 容器(底部导航栏)
│   │   └── view/
│   │       └── app_tab.dart                 # StatefulShellRoute 子壳 + 底部导航逻辑
│   │
│   ├── login/                               # 登录 ── 已实现
│   │   ├── di/
│   │   │   └── auth_providers.dart          # authRepositoryProvider / loginUseCaseProvider
│   │   ├── presentation/
│   │   │   ├── login_view_model.dart        # @riverpod ViewModel(生成 login_view_model.g.dart)
│   │   │   └── login_state.dart             # @freezed State(生成 login_state.freezed.dart)
│   │   ├── usecases/
│   │   │   └── login_usecase.dart           # 格式校验 → Repository → User Entity
│   │   └── view/
│   │       └── login_page.dart              # 登录页
│   │
│   ├── chat/                                # 聊天 ── 开发中
│   │   ├── presentation/
│   │   │   └── chat_view_model.dart         # @riverpod ViewModel
│   │   └── view/
│   │       ├── chat_page.dart               # 会话列表页(Tab 1)
│   │       └── chat_detail_page.dart        # 聊天详情页
│   │
│   ├── contact/                             # 通讯录 ── 骨架
│   │   └── view/
│   │       └── contact_page.dart            # 通讯录页(Tab 2)
│   │
│   └── settings/                            # 设置 ── 已实现(主题切换)
│       ├── di/
│       │   └── settings_providers.dart      # settingsRepositoryProvider(待 storage_sdk 接入)
│       ├── presentation/
│       │   ├── settings_view_model.dart     # @riverpod ViewModel(设置页导航)
│       │   └── theme_view_model.dart        # @riverpod ViewModel(生成 theme_view_model.g.dart)
│       ├── usecases/
│       │   └── set_theme_usecase.dart       # 主题切换用例
│       └── view/
│           ├── settings_page.dart           # 设置主页(Tab 3)
│           ├── theme_view.dart              # 主题选择页
│           └── widgets/
│               ├── settings_section_header.dart
│               └── theme_option_tile.dart
│
├── domain/                                  # Domain 层(纯 Dart,零 Flutter / 零网络依赖)
│   ├── entities/
│   │   └── user.dart                        # 用户实体
│   │   # message / conversation / contact  待开发
│   └── repositories/
│       └── auth_repository.dart             # abstract interface
│       # message / chat / contact_repository  待开发
│
├── data/                                    # Data 层(implements domain 接口)
│   ├── repositories/
│   │   └── auth_repository_impl.dart        # 认证仓库
│   │   # message / chat / contact  待开发
│   ├── local/
│   │   └── drift/                           # Drift 本地数据库
│   │       ├── app_database.dart            # @DriftDatabase 定义 + onUpgrade 自动补列
│   │       # database_connection.dart 已迁移至 storage_sdk(数据库生命周期统一在 SDK 层管理)
│   │       ├── mapper/
│   │       │   └── drift_path_mapper.dart   # Drift 路径映射工具
│   │       └── tables/
│   │           └── users.dart               # Users 表定义
│   ├── remote/                              # Request 文件(一个端点一个文件)
│   │   ├── login_request.dart               # 登录
│   │   ├── logout_request.dart              # 登出
│   │   ├── get_profile_request.dart         # 获取用户信息
│   │   └── upload_file_request.dart         # 文件上传
│   │   # send_message / 其他业务端点  待开发
│   └── models/                              # 持久化 DTO(@JsonSerializable)
│       └── user_dto.dart                    # 用户持久化 DTO
│       # message / conversation / contact_dto  待开发
│
└── core/                                    # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
    ├── foundation/                          # 基础配置(各为单独文件,非子目录)
    │   ├── api_paths.dart                   # API 路径常量(ApiPaths.authLogin 等)
    │   ├── config.dart                      # 运行时配置(AppConfig,通过 --dart-define-from-file 注入)
    │   ├── constants.dart                   # 全局常量(AppConstants:重试次数 / 退避延迟 / 超时等)
    │   ├── errors.dart                      # 统一异常体系
    │   ├── extensions.dart                  # Dart 扩展方法(String / DateTime / List 等)
    │   ├── logger.dart                      # 日志门面(分级日志,生产环境自动关闭 debug)
    │   ├── types.dart                       # 通用类型(Result<T> / typedefs / Unit)
    │   └── utils.dart                       # 工具函数(纯函数,无副作用)
    │
    ├── services/                            # 跨模块服务(有状态,作为独立 Provider)
    │   ├── app_initializer.dart             # 启动初始化编排(按序初始化各依赖)
    │   ├── network_backoff_debouncer.dart   # 网络恢复退避防抖(4s→8s→...→60s,2min 重置)
    │   ├── network_monitor.dart             # 网络状态监听(connectivity_plus)
    │   └── socket_manager.dart              # WebSocket 生命周期(连接/断开/重连编排)
    │
    └── ui/                                  # Core UI(设计系统 + 可复用组件)
        ├── base/                            # 设计 Token
        │   ├── assets.dart                  # 静态资源路径常量(AppAssets:logo / 占位图)
        │   ├── icons.dart                   # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态)
        │   ├── app_theme.dart               # ThemeData 组装(Light / Dark)
        │   ├── colors.dart                  # 颜色体系(品牌色 / 语义色 / 灰阶)
        │   ├── context_theme_ext.dart       # BuildContext 主题扩展(context.theme / context.colors)
        │   └── font.dart                    # 字体(TextStyle 定义 + textTheme(brightness))
        ├── components/                      # 原子组件
        │   └── app_button.dart              # 按钮
        │   # app_text_field / app_avatar / app_badge 等  待开发
        └── composites/                      # 组合组件(目录预留,待开发)
            # app_dialog / app_toast / app_empty_state 等

2.3 整体分层图(MVVM + Riverpod 数据流)

图表说明

展示完整的五层架构,标注 MVVM 角色映射和 Riverpod 驱动的单向数据流。

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 → NetworksSdkApi"] Step4["④ state = state.copyWith(
messages: [..., newMsg])"] Step5["⑤ ref.watch(chatVM) 检测变化
→ ConsumerWidget 自动 rebuild"] Step6["⑥ UI 展示最新消息列表"] end Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> Step6 subgraph DirMapping["目录 ↔ 角色"] direction TB D1["chat_page.dart → View"] D2["chat_view_model.dart → ViewModel"] D5["domain/entities/message.dart → Model"] end style MVVM fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style RiverpodFlow fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style DirMapping fill:#fff4e6,stroke:#f57c00,stroke-width:2px

两大核心逻辑

1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。

2. Riverpod 单向数据流:用户操作 → ref.read(vm.notifier).action() → ViewModel 处理逻辑 → state = newStateref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。

3. Widget 纯展示原则build() 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。

  • 派生显示值必须是 State getter:凡是需要从 State 字段推导出另一个值(文本、颜色、样式等),一律写成 State 的 getter,Widget 只读取结果。例如:String get buttonLabel => testStarted ? '结束' : '开始';,Widget 写 state.buttonLabel,不在 build() 里写三元。
  • 禁止 build() 内定义局部计算变量final isSelected = current == mode 这类从构造参数/state 派生值的局部变量,不属于 build()。组件应接受已算好的 bool isSelected 参数,由父级或 State getter 负责计算。
  • 导航、CRUD、状态变更全部在 ViewModel 中完成,Widget 只转发事件:onTap: () => ref.read(vm.notifier).doAction()
  • demo/测试页面同样适用:demo 代码是示范,别人会照着模仿,写法必须与正式页面完全一致。


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

Riverpod 核心概念

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

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

Clean Architecture 分层说明

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

分层职责

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

依赖倒置原则(DIP)

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

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 NetworksSdkApi _client;
  final MessageLocalDataSource _localDataSource;

  @override
  Future<List<Message>> getMessages(String chatId) async {
    // 实现数据获取逻辑
  }
}

// Presentation 层使用接口
class ChatViewModel {
  final ChatRepository _repository;  // 依赖接口,不依赖实现

  Future<void> loadMessages() async {
    final messages = await _repository.getMessages(chatId);
    // ...
  }
}

Repository 模式

作用:将数据访问逻辑封装起来,对上层提供统一的数据访问接口

优势

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 层是应用的最外层,负责:

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、Avatar Atomic Design 原则
L3: Business Components
业务组件
包含业务逻辑的复用组件 MessageBubble、ChatItem、ContactCard Feature 级别复用
L4: Pages
页面
完整的页面,组合各种组件 ChatPage、ChatListPage、ContactPage Feature 独有

L1: Design System(设计系统)

核心原则:与 Figma 设计稿完全对应,确保设计与实现一致。

1.1 Colors(颜色)
/// 颜色定义 - 与 Figma 设计稿对应
class AppColors {
  // Primary Colors - 主色
  static const primary = Color(0xFF667EEA);
  static const primaryDark = Color(0xFF5568D3);
  static const primaryLight = Color(0xFF8B9FFF);

  // Secondary Colors - 辅助色
  static const secondary = Color(0xFF764BA2);
  static const secondaryDark = Color(0xFF5E3882);
  static const secondaryLight = Color(0xFF9B6FC4);

  // Neutral Colors - 中性色
  static const black = Color(0xFF000000);
  static const white = Color(0xFFFFFFFF);
  static const gray900 = Color(0xFF1A1A1A);
  static const gray800 = Color(0xFF2D2D2D);
  static const gray700 = Color(0xFF404040);
  static const gray600 = Color(0xFF5C5C5C);
  static const gray500 = Color(0xFF737373);
  static const gray400 = Color(0xFF999999);
  static const gray300 = Color(0xFFBFBFBF);
  static const gray200 = Color(0xFFE6E6E6);
  static const gray100 = Color(0xFFF5F5F5);
  static const gray50 = Color(0xFFFAFAFA);

  // Semantic Colors - 语义色
  static const success = Color(0xFF10B981);
  static const warning = Color(0xFFF59E0B);
  static const error = Color(0xFFEF4444);
  static const info = Color(0xFF3B82F6);
}
1.2 Typography(字体)
/// 字体定义 - 与 Figma 设计稿对应
class AppTypography {
  // Display - 展示标题
  static const displayLarge = TextStyle(
    fontSize: 57,
    fontWeight: FontWeight.w700,
    height: 1.12,
  );

  static const displayMedium = TextStyle(
    fontSize: 45,
    fontWeight: FontWeight.w700,
    height: 1.16,
  );

  // Headline - 标题
  static const headlineLarge = TextStyle(
    fontSize: 32,
    fontWeight: FontWeight.w600,
    height: 1.25,
  );

  static const headlineMedium = TextStyle(
    fontSize: 28,
    fontWeight: FontWeight.w600,
    height: 1.29,
  );

  // Body - 正文
  static const bodyLarge = TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w400,
    height: 1.5,
  );

  static const bodyMedium = TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w400,
    height: 1.43,
  );

  // Label - 标签
  static const labelLarge = TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w500,
    height: 1.43,
  );

  static const labelMedium = TextStyle(
    fontSize: 12,
    fontWeight: FontWeight.w500,
    height: 1.33,
  );
}
1.3 Design Tokens(基础定义)
/// 基础定义 - 间距、圆角、阴影等
class AppTokens {
  // Spacing - 间距(8pt 网格系统)
  static const spacing4 = 4.0;
  static const spacing8 = 8.0;
  static const spacing12 = 12.0;
  static const spacing16 = 16.0;
  static const spacing20 = 20.0;
  static const spacing24 = 24.0;
  static const spacing32 = 32.0;
  static const spacing40 = 40.0;
  static const spacing48 = 48.0;

  // Border Radius - 圆角
  static const radiusSmall = 4.0;
  static const radiusMedium = 8.0;
  static const radiusLarge = 12.0;
  static const radiusXLarge = 16.0;
  static const radiusFull = 9999.0;

  // Elevation - 阴影
  static const elevationNone = 0.0;
  static const elevationLow = 2.0;
  static const elevationMedium = 4.0;
  static const elevationHigh = 8.0;
}
1.4 Theme(主题 - 黑暗模式)
/// 主题定义 - 支持亮色/暗色模式
class AppTheme {
  // Light Theme
  static ThemeData light = ThemeData(
    brightness: Brightness.light,
    primaryColor: AppColors.primary,
    scaffoldBackgroundColor: AppColors.white,
    colorScheme: const ColorScheme.light(
      primary: AppColors.primary,
      secondary: AppColors.secondary,
      error: AppColors.error,
      surface: AppColors.white,
      background: AppColors.gray50,
    ),
    textTheme: TextTheme(
      displayLarge: AppTypography.displayLarge,
      headlineMedium: AppTypography.headlineMedium,
      bodyLarge: AppTypography.bodyLarge,
    ),
  );

  // Dark Theme
  static ThemeData dark = ThemeData(
    brightness: Brightness.dark,
    primaryColor: AppColors.primary,
    scaffoldBackgroundColor: AppColors.gray900,
    colorScheme: const ColorScheme.dark(
      primary: AppColors.primary,
      secondary: AppColors.secondary,
      error: AppColors.error,
      surface: AppColors.gray800,
      background: AppColors.black,
    ),
    textTheme: TextTheme(
      displayLarge: AppTypography.displayLarge.copyWith(color: AppColors.white),
      headlineMedium: AppTypography.headlineMedium.copyWith(color: AppColors.white),
      bodyLarge: AppTypography.bodyLarge.copyWith(color: AppColors.gray100),
    ),
  );
}
1.5 Figma 设计稿对应规范

重要原则:代码中的命名必须与 Figma 设计稿完全对应

Figma 命名 代码命名 说明
Primary/Main AppColors.primary 主色
Button/Primary AppButton.primary() 主按钮
Text/Headline/Large AppTypography.headlineLarge 大标题
Spacing/16 AppTokens.spacing16 16pt 间距
Radius/Medium AppTokens.radiusMedium 中等圆角

好处

  • 设计师与开发者使用相同的术语,沟通零障碍
  • 代码审查时可直接对照 Figma 检查实现
  • 设计变更时快速定位需要修改的代码

L2: Foundation(基础组件 - Atomic Design)

遵循 Atomic Design 原则,分为三个层级:

2.1 Atoms(原子组件)

最小的 UI 单元,不可再分。

/// AppButton - 按钮原子组件
class AppButton extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;
  final ButtonVariant variant;
  final ButtonSize size;

  const AppButton.primary({
    required this.text,
    this.onPressed,
    this.size = ButtonSize.medium,
  }) : variant = ButtonVariant.primary;

  const AppButton.secondary({
    required this.text,
    this.onPressed,
    this.size = ButtonSize.medium,
  }) : variant = ButtonVariant.secondary;

  @override
  Widget build(BuildContext context) {
    // 使用 Design System 的颜色和字体
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: _getBackgroundColor(),
        foregroundColor: _getForegroundColor(),
        padding: _getPadding(),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(AppTokens.radiusMedium),
        ),
      ),
      child: Text(text, style: _getTextStyle()),
    );
  }
}

/// AppTextField - 文本框原子组件
class AppTextField extends StatelessWidget {
  final String? label;
  final String? hint;
  final TextEditingController? controller;

  const AppTextField({
    this.label,
    this.hint,
    this.controller,
  });

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      style: AppTypography.bodyMedium,
      decoration: InputDecoration(
        labelText: label,
        hintText: hint,
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(AppTokens.radiusMedium),
        ),
      ),
    );
  }
}
2.2 Molecules(分子组件)

由多个原子组件组合而成。

/// SearchBar - 搜索栏分子组件
class SearchBar extends StatelessWidget {
  final String hint;
  final ValueChanged<String>? onChanged;

  const SearchBar({
    required this.hint,
    this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(AppTokens.spacing12),
      child: Row(
        children: [
          Icon(Icons.search, color: AppColors.gray500),
          SizedBox(width: AppTokens.spacing8),
          Expanded(
            child: AppTextField(
              hint: hint,
              controller: TextEditingController(),
            ),
          ),
        ],
      ),
    );
  }
}
2.3 Organisms(有机组件)

由原子和分子组件组合成的复杂组件。

/// UserCard - 用户卡片有机组件
class UserCard extends StatelessWidget {
  final String name;
  final String avatar;
  final String lastMessage;
  final VoidCallback? onTap;

  const UserCard({
    required this.name,
    required this.avatar,
    required this.lastMessage,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Avatar(url: avatar),
        title: Text(name, style: AppTypography.bodyLarge),
        subtitle: Text(lastMessage, style: AppTypography.bodyMedium),
        trailing: AppButton.secondary(
          text: '发消息',
          onPressed: onTap,
        ),
      ),
    );
  }
}

L3: Business Components(业务组件)

包含业务逻辑的组件,通常与 Feature 相关。

/// MessageBubble - 消息气泡(业务组件)
class MessageBubble extends ConsumerWidget {
  final Message message;

  const MessageBubble({required this.message});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isSender = message.senderId == ref.watch(currentUserProvider).id;

    return Align(
      alignment: isSender ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: EdgeInsets.symmetric(
          horizontal: AppTokens.spacing16,
          vertical: AppTokens.spacing8,
        ),
        padding: EdgeInsets.all(AppTokens.spacing12),
        decoration: BoxDecoration(
          color: isSender ? AppColors.primary : AppColors.gray200,
          borderRadius: BorderRadius.circular(AppTokens.radiusLarge),
        ),
        child: Text(
          message.content,
          style: AppTypography.bodyMedium.copyWith(
            color: isSender ? AppColors.white : AppColors.black,
          ),
        ),
      ),
    );
  }
}

L4: Pages(页面)

完整的页面,组合各种组件,连接 ViewModel。

/// ChatPage - 聊天页面
class ChatPage extends ConsumerWidget {
  final String chatId;

  const ChatPage({required this.chatId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(chatViewModelProvider(chatId));

    return Scaffold(
      appBar: AppBar(title: Text(state.chatName)),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: state.messages.length,
              itemBuilder: (context, index) {
                return MessageBubble(message: state.messages[index]);
              },
            ),
          ),
          ChatInputBar(
            onSend: (text) {
              ref.read(chatViewModelProvider(chatId).notifier).sendMessage(text);
            },
          ),
        ],
      ),
    );
  }
}

UI 层目录结构

lib/
├── core/ui/
│   ├── base/                # L1: 基础定义(已实现)
│   │   ├── colors.dart      # 颜色体系(品牌色 / 语义色 / 灰阶)
│   │   ├── font.dart        # TextStyle 定义 + textTheme(brightness)
│   │   └── app_theme.dart   # ThemeData 组装(Light / Dark)
│   │
│   ├── components/          # L2: 原子组件
│   │   └── app_button.dart  # 按钮(已实现)
│   │   # app_text_field / app_icon / app_avatar 等  待开发
│   │
│   └── composites/          # L3: 组合组件
│       └── app_dialog.dart  # 确认弹窗(已实现)
│       # app_action_sheet / app_toast 等  待开发
│
└── features/
    └── chat/
        └── view/            # L4: 页面 + Feature 专属组件(待开发)
            ├── chat_page.dart
            └── widgets/
                ├── message_bubble.dart
                ├── message_input_bar.dart
                └── message_list_view.dart

核心价值

  • 设计系统:确保设计与实现一致,与 Figma 完全对应
  • 原子设计:从小到大构建组件,提升复用性
  • 清晰分层:基础组件 vs 业务组件,职责明确
  • 主题支持:亮色/暗色模式统一管理
  • 易于维护:设计变更只需修改 Design System

3.2 多平台适配

核心理念:一套代码适配多个平台(iOS、Android、Web、Windows、macOS、Linux),通过平台检测和自适应组件实现平台特定的 UI 和交互。

支持的平台

平台类型 具体平台 设计规范 特点
移动端 iOS、Android Cupertino / Material Design 触摸交互、竖屏优先
桌面端 Windows、macOS、Linux Fluent / macOS / GNOME 鼠标键盘、大屏幕
Web 端 浏览器 响应式设计 跨浏览器兼容

3.2.1 平台检测与适配策略

/// 平台工具类 - 统一平台检测
class PlatformAdapter {
  // 平台类型判断
  static bool get isMobile => Platform.isIOS || Platform.isAndroid;
  static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
  static bool get isIOS => Platform.isIOS;
  static bool get isAndroid => Platform.isAndroid;
  static bool get isWeb => kIsWeb;
  static bool get isMacOS => Platform.isMacOS;
  static bool get isWindows => Platform.isWindows;

  // 设备类型判断(基于屏幕尺寸)
  static DeviceType getDeviceType(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    if (width < 600) return DeviceType.mobile;
    if (width < 1200) return DeviceType.tablet;
    return DeviceType.desktop;
  }

  // 获取平台特定的设计风格
  static DesignStyle get designStyle {
    if (isIOS) return DesignStyle.cupertino;
    if (isAndroid) return DesignStyle.material;
    if (isMacOS) return DesignStyle.macos;
    if (isWindows) return DesignStyle.fluent;
    return DesignStyle.material; // 默认
  }
}

enum DeviceType { mobile, tablet, desktop }
enum DesignStyle { material, cupertino, fluent, macos }

3.2.2 响应式布局

根据屏幕尺寸自动调整布局:

/// 响应式布局组件
class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget? tablet;
  final Widget? desktop;

  const ResponsiveLayout({
    required this.mobile,
    this.tablet,
    this.desktop,
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // 桌面布局(>= 1200px)
        if (constraints.maxWidth >= 1200) {
          return desktop ?? tablet ?? mobile;
        }
        // 平板布局(>= 600px)
        else if (constraints.maxWidth >= 600) {
          return tablet ?? mobile;
        }
        // 手机布局(< 600px)
        else {
          return mobile;
        }
      },
    );
  }
}

/// 使用示例
class ChatPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ResponsiveLayout(
      // 手机:单栏布局
      mobile: SingleColumnChatView(),
      // 平板:两栏布局(会话列表 + 聊天)
      tablet: TwoColumnChatView(),
      // 桌面:三栏布局(联系人 + 会话列表 + 聊天)
      desktop: ThreeColumnChatView(),
    );
  }
}

3.2.3 平台自适应组件

根据平台自动选择 Material 或 Cupertino 风格:

/// 平台自适应按钮
class PlatformButton extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;

  const PlatformButton({
    required this.text,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    // iOS 使用 Cupertino 风格
    if (PlatformAdapter.isIOS) {
      return CupertinoButton(
        onPressed: onPressed,
        color: AppColors.primary,
        child: Text(text),
      );
    }
    // Android 和其他平台使用 Material 风格
    else {
      return ElevatedButton(
        onPressed: onPressed,
        child: Text(text),
      );
    }
  }
}

/// 平台自适应导航栏
class PlatformAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final List<Widget>? actions;

  const PlatformAppBar({
    required this.title,
    this.actions,
  });

  @override
  Widget build(BuildContext context) {
    // iOS 使用 CupertinoNavigationBar
    if (PlatformAdapter.isIOS) {
      return CupertinoNavigationBar(
        middle: Text(title),
        trailing: actions != null ? Row(children: actions!) : null,
      );
    }
    // Android 使用 Material AppBar
    else {
      return AppBar(
        title: Text(title),
        actions: actions,
      );
    }
  }

  @override
  Size get preferredSize => Size.fromHeight(56);
}

/// 平台自适应对话框
class PlatformDialog {
  static Future<bool?> showConfirm(
    BuildContext context, {
    required String title,
    required String content,
  }) {
    // iOS 使用 CupertinoAlertDialog
    if (PlatformAdapter.isIOS) {
      return showCupertinoDialog<bool>(
        context: context,
        builder: (context) => CupertinoAlertDialog(
          title: Text(title),
          content: Text(content),
          actions: [
            CupertinoDialogAction(
              child: Text('取消'),
              onPressed: () => Navigator.pop(context, false),
            ),
            CupertinoDialogAction(
              child: Text('确定'),
              isDestructiveAction: true,
              onPressed: () => Navigator.pop(context, true),
            ),
          ],
        ),
      );
    }
    // Android 使用 Material AlertDialog
    else {
      return showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: Text(title),
          content: Text(content),
          actions: [
            TextButton(
              child: Text('取消'),
              onPressed: () => Navigator.pop(context, false),
            ),
            TextButton(
              child: Text('确定'),
              onPressed: () => Navigator.pop(context, true),
            ),
          ],
        ),
      );
    }
  }
}

3.2.4 平台特定交互

/// 平台特定的滑动返回手势
class PlatformScaffold extends StatelessWidget {
  final Widget body;
  final PreferredSizeWidget? appBar;

  const PlatformScaffold({
    required this.body,
    this.appBar,
  });

  @override
  Widget build(BuildContext context) {
    final scaffold = Scaffold(
      appBar: appBar,
      body: body,
    );

    // iOS 支持侧滑返回
    if (PlatformAdapter.isIOS) {
      return CupertinoPageScaffold(
        navigationBar: appBar as ObstructingPreferredSizeWidget?,
        child: body,
      );
    }

    return scaffold;
  }
}

/// 平台特定的右键菜单(桌面端)
class PlatformContextMenu extends StatelessWidget {
  final Widget child;
  final List<ContextMenuItem> menuItems;

  const PlatformContextMenu({
    required this.child,
    required this.menuItems,
  });

  @override
  Widget build(BuildContext context) {
    // 桌面端支持右键菜单
    if (PlatformAdapter.isDesktop) {
      return GestureDetector(
        onSecondaryTapDown: (details) {
          _showContextMenu(context, details.globalPosition);
        },
        child: child,
      );
    }

    // 移动端使用长按显示菜单
    return GestureDetector(
      onLongPress: () {
        _showMobileMenu(context);
      },
      child: child,
    );
  }

  void _showContextMenu(BuildContext context, Offset position) {
    // 显示桌面端右键菜单
  }

  void _showMobileMenu(BuildContext context) {
    // 显示移动端底部菜单
  }
}

3.2.5 屏幕尺寸断点

/// 屏幕断点定义
class ScreenBreakpoints {
  // 手机
  static const double mobile = 0;
  static const double mobileMax = 599;

  // 平板
  static const double tablet = 600;
  static const double tabletMax = 1199;

  // 桌面
  static const double desktop = 1200;
  static const double desktopMax = 1919;

  // 大屏
  static const double ultraWide = 1920;

  // 判断当前断点
  static ScreenSize getSize(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    if (width < tablet) return ScreenSize.mobile;
    if (width < desktop) return ScreenSize.tablet;
    if (width < ultraWide) return ScreenSize.desktop;
    return ScreenSize.ultraWide;
  }
}

enum ScreenSize { mobile, tablet, desktop, ultraWide }

/// 响应式间距
class ResponsiveSpacing {
  static double get(BuildContext context, {
    double mobile = 16,
    double tablet = 24,
    double desktop = 32,
  }) {
    final size = ScreenBreakpoints.getSize(context);
    switch (size) {
      case ScreenSize.mobile:
        return mobile;
      case ScreenSize.tablet:
        return tablet;
      case ScreenSize.desktop:
      case ScreenSize.ultraWide:
        return desktop;
    }
  }
}

3.2.6 多平台目录结构

lib/
├── core/ui/
│   ├── base/                      # 基础定义(已实现:colors / font / app_theme)
│   │   ├── colors.dart            # 颜色体系(品牌色 / 语义色 / 灰阶)
│   │   ├── font.dart              # TextStyle 定义 + textTheme(brightness)
│   │   └── app_theme.dart         # ThemeData 组装(可按平台扩展 Material / Cupertino)
│   │
│   ├── components/                  # 基础组件
│   │   ├── app_button.dart
│   │   ├── app_text_field.dart
│   │   └── app_avatar.dart
│   │
│   ├── composites/                  # 业务组合组件
│   │   ├── app_alert_dialog.dart
│   │   └── app_action_sheet.dart
│   │
│   └── platform/                    # 平台适配(可选)
│       ├── platform_adapter.dart    # 平台检测
│       ├── responsive_layout.dart   # 响应式布局
│       └── screen_breakpoints.dart  # 屏幕断点
│
└── features/
    └── chat/
        └── view/
            ├── chat_page.dart             # 主入口(平台自适应)
            └── layouts/                   # 不同布局
                ├── mobile_layout.dart     # 手机布局
                ├── tablet_layout.dart     # 平板布局
                └── desktop_layout.dart    # 桌面布局

多平台适配核心价值

  • 一套代码:维护成本低,所有平台同步更新
  • 平台原生感:自动适配平台特定的设计规范和交互
  • 响应式设计:自动适应不同屏幕尺寸
  • 性能优化:根据平台特性优化渲染和交互
  • 用户体验一致:核心功能在所有平台保持一致

3.2.7 平台适配最佳实践

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

3.3 Feature UI 组织

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

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 - 聊天功能

Chat List Feature - 会话列表功能

Contact Feature - 联系人功能

Search Feature - 搜索功能

Call Feature - 通话功能

设计原则:UI 层的每个页面都在其对应的 Feature 目录下,与该 Feature 的 Presentation 层和 Domain 层垂直对齐,形成高内聚的功能模块。



路由系统(go_router)

路由是什么

路由就是「页面地址 → 页面」的映射表。打开 App 时系统根据当前地址决定显示哪个页面,点击按钮时通过地址跳转到另一个页面。

Shell 是什么

Shell(壳层)是一个持久存在的 UI 框架,内容区域在里面切换,而框架本身不销毁。类比一下:

在本项目里:

AppTab 就是 Shell 组件,它只负责渲染底部导航栏和容纳当前页面内容,自身不包含任何业务逻辑。

为什么禁止使用 Navigator.push

传统写法:

// 禁止 ❌
Navigator.push(context, MaterialPageRoute(builder: (_) => const ThemeView()));

禁止原因:

正确写法:

// 正确 ✅
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,负责:

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

ChatListViewModel

ContactViewModel

SearchViewModel

CallViewModel

设计原则:每个 Feature 的 ViewModel 独立管理该 Feature 的状态,直接调用 Repository 执行数据操作。当业务逻辑复杂(多步编排、跨模块协调)时,可提取 UseCase 封装。



Domain 层模块详解

5.1 Domain 层职责

Domain 层是整洁架构的核心,分为两部分:

Feature 专属 Domain(features/*/domain/)

全局共享 Domain(domain/)

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

Chat List Feature Use Cases

Contact Feature Use Cases

Call Feature Use Cases

5.7 Repository 接口

位置domain/repositories/(全局目录)

Domain 层只定义接口,不包含实现:

关键原则

1. UseCase 按需创建:只在多步编排、跨模块协调等复杂场景使用,简单 CRUD 直接调 Repository

2. UseCase 在 Feature 目录:需要时,放在 features/*/usecases/ 下

3. Repository 接口在全局 domain/:所有 Repository 接口定义在 domain/repositories/ 下

4. 依赖倒置:UseCase / ViewModel 依赖 Repository 接口,不依赖具体实现



Data 层模块详解

6.1 Data 层职责

Data 层是全局目录,负责:

6.2 Data 层目录结构

lib/data/
├── repositories/                               # Repository 实现
│   ├── message_repository_impl.dart            # 实现 MessageRepository 接口
│   ├── chat_repository_impl.dart               # 实现 ChatRepository 接口
│   ├── contact_repository_impl.dart            # 实现 ContactRepository 接口
│   ├── user_repository_impl.dart               # 实现 UserRepository 接口
│   └── call_repository_impl.dart               # 实现 CallRepository 接口
│
├── local/                                      # 本地数据源
│   ├── message_local_ds.dart                   # 消息本地数据源
│   ├── chat_local_ds.dart                      # 会话本地数据源
│   ├── contact_local_ds.dart                   # 联系人本地数据源
│   ├── user_local_ds.dart                      # 用户本地数据源
│   │
│   ├── drift/                                  # Drift 数据库
│   │   ├── app_database.dart                   # Drift 数据库定义
│   │   ├── app_database.g.dart                 # Drift 生成代码
│   │   # database_connection.dart 已迁移至 storage_sdk(数据库连接与 Isolate 生命周期由 SDK 层统一管理)
│   │   ├── tables/                             # 表定义
│   │   │   ├── message_table.dart              # 消息表
│   │   │   ├── conversation_table.dart         # 会话表
│   │   │   └── user_table.dart                 # 用户表
│   │   ├── daos/                               # 数据访问对象
│   │   │   ├── message_dao.dart                # 消息 DAO
│   │   │   ├── conversation_dao.dart           # 会话 DAO
│   │   │   └── user_dao.dart                   # 用户 DAO
│   │   ├── migrations/                         # 数据库迁移
│   │   │   ├── migration_v1.dart               # V1 迁移脚本
│   │   │   └── migration_runner.dart           # 迁移执行器
│   │   └── mappers/                            # DB ↔ DTO 映射
│   │       ├── message_mapper.dart             # 消息映射
│   │       └── conversation_mapper.dart        # 会话映射
│   │
│   └── storage/                                # 其他本地存储
│       ├── preference_storage.dart             # SharedPreferences 封装
│       ├── file_storage.dart                   # 文件存储管理
│       └── image_cache.dart                    # 图片缓存
│
├── remote/                                     # Request 文件(一个端点一个文件,Repository 直接调 NetworksSdkApi)
│   ├── login_request.dart                      # 登录端点
│   ├── logout_request.dart                     # 登出端点
│   ├── send_message_request.dart               # 发消息端点
│   └── ...                                     # 其他端点 Request 文件
│
├── cache/                                      # 缓存
│   ├── cache_manager.dart                      # 缓存管理器
│   └── cache_policies.dart                     # 缓存策略
│
└── models/                                     # DTO(统一归口,local / remote 共用)
    ├── message_dto.dart                        # 消息 DTO
    ├── conversation_dto.dart                   # 会话 DTO
    ├── user_dto.dart                           # 用户 DTO
    ├── contact_dto.dart                        # 联系人 DTO
    └── call_dto.dart                           # 通话 DTO

6.3 Repository 实现

flowchart TD Domain[Domain Layer
domain/repositories/
Repository 接口] -.实现.-> Repo[Data Layer
data/repositories/
Repository 实现] Repo -->|读取| LocalDS[Local DataSource
data/local/] Repo -->|请求| SdkApi[NetworksSdkApi
networks_sdk] Repo -->|缓存| Cache[Cache Manager
data/cache/] LocalDS -->|Drift| DB[(Database)] SdkApi -->|HTTP/WebSocket| API[API Server] Cache -->|内存| Memory[Memory Cache] style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style LocalDS fill:#c8e6c9,stroke:#388e3c style SdkApi fill:#c8e6c9,stroke:#388e3c style Cache fill:#c8e6c9,stroke:#388e3c

6.4 Data Source 详解

Local DataSource(data/local/)

// 示例:MessageLocalDataSource
class MessageLocalDataSource {
  final DriftSDK _db;

  Future<List<MessageDTO>> getMessages(String chatId) {
    return _db.query('messages', where: 'chat_id = ?', whereArgs: [chatId]);
  }
}

Request 文件(data/remote/)

// 示例:Repository 直接调用 Request
// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
  final NetworksSdkApi _client;

  Future<SendMessageData?> sendMessage({
    required String chatId,
    required String content,
  }) {
    return _client.executeRequest(
      SendMessageRequest(chatId: chatId, content: content),
    );
  }
}

Cache Manager(data/cache/)

6.5 DTO Models(data/models/)

Data Transfer Objects 用于数据传输:

// 示例: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. 请求远程| SdkApi2[NetworksSdkApi] SdkApi2 -->|DTO| RepoImpl RepoImpl -->|转换| Entity[Entity] Entity -->|返回| UC style RepoInterface fill:#f3e5f5,stroke:#7b1fa2 style RepoImpl fill:#e8f5e9,stroke:#388e3c style UC fill:#f3e5f5,stroke:#7b1fa2 style Entity fill:#f3e5f5,stroke:#7b1fa2

关键原则

1. Data 层是全局目录:所有 Repository 实现都在 data/repositories/ 下

2. 实现全局接口:实现 domain/repositories/ 中定义的接口

3. 统一数据源管理:按类型组织(local/remote/cache),不按 Feature 划分

4. DTO 与 Entity 分离:DTO 在 Data 层,Entity 在 Domain 层



Core 层模块详解

7.1 Core 层职责

Core 层保留主 App 内部两部分,类似 Apple 的 CoreFoundation / CoreUI 的关系:

多语言国际化(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)包含:

注意:网络/存储/加密/媒体/RTC/推送/协议/原生通信/多语言国际化等 SDK 能力已提取为独立 Package(packages/*_sdk),由 Melos 统一管理,主 App 通过 pubspec.yaml 引用。详见「Mono-Repo 架构」章节和下方「7.3 核心 SDK(独立 Package)」。

7.2 基础设施

Constants(core/foundation/constants/)

Config(core/foundation/config.dart)

Errors(core/foundation/errors/)

Logger(core/foundation/logger/)

Types(core/foundation/types/)

7.3 核心 SDK(独立 Package)

以下 SDK 均为 packages/ 下的 Flutter Plugin(含 Android/iOS 原生代码入口),主 App 通过 import 'package:xxx_sdk/xxx_sdk.dart' 引用。所有 SDK 遵循 Facade + Wiring 内部架构(见「SDK 内部架构」章节)。

Networks SDK(packages/networks_sdk/)

HTTP + WebSocket 客户端 SDK(Flutter Plugin 声明,无实际 Native 代码)。App 层通过 import 'package:networks_sdk/networks_sdk.dart' 引用,遵循 Facade + Wiring 内部架构。

Package 目录结构
packages/networks_sdk/
├── pubspec.yaml
├── build.yaml                              # @ApiRequest 代码生成器注册
└── lib/
    ├── networks_sdk.dart                   # barrel file(统一导出,见下方「导出清单」)
    └── src/
        ├── annotations/
        │   └── api_request.dart            # @ApiRequest 注解定义
        ├── generator/
        │   ├── api_request_generator.dart  # build_runner 代码生成器实现
        │   └── builder.dart                # SharedPartBuilder 入口
        ├── data/
        │   ├── datasources/
        │   │   ├── http/
        │   │   │   ├── api_client.dart              # Dio REST 客户端
        │   │   │   ├── token_refresh_manager.dart   # Token 刷新管理(竞态安全 + 超时 + 时间窗口复用 + 主动刷新)
        │   │   │   └── interceptor/
        │   │   │       ├── auth_interceptor.dart        # Token + 默认 header 注入
        │   │   │       ├── encryption_interceptor.dart  # 请求加密 / 响应解密(预留给 cipher_guard_sdk)
        │   │   │       ├── retry_interceptor.dart       # Token 刷新 + 瞬态错误重试 + 业务错误钩子
        │   │   │       └── logging_interceptor.dart     # 请求/响应日志
        │   │   ├── socket/
        │   │   │   └── socket_client.dart           # WebSocket 长连接(心跳/重连/Stream/Token 热更新/加密钩子)
        │   │   └── networks_sdk_method_channel_datasource.dart  # 统一执行入口(executeRequest / executeDownload)
        │   ├── dto/
        │   │   ├── api_requestable.dart         # 请求基类 + fromJson 注册表 + 解码扩展
        │   │   └── api_response_wrapper.dart    # { code, message/msg, data } 响应包装解析
        │   └── repositories/
        │       ├── networks_sdk_repository_impl.dart
        │       └── networks_messaging_repository_impl.dart
        ├── domain/
        │   ├── entities/
        │   │   ├── api_error.dart               # @freezed HTTP 错误联合类型(7 变体,含 cancelled)
        │   │   ├── encrypted_request.dart       # 加密请求结果数据类(path / headers / body 覆盖)
        │   │   ├── socket_error.dart             # @freezed WebSocket 错误联合类型
        │   │   ├── socket_connection_state.dart  # 连接状态 enum
        │   │   ├── http_method.dart              # GET / POST / PUT / DELETE / PATCH
        │   │   └── api_request_type.dart         # request / login / upload / stream / download
        │   └── repositories/
        │       ├── networks_sdk_repository.dart
        │       └── networks_messaging_repository.dart
        └── presentation/
            ├── facade/
            │   ├── networks_sdk_api.dart         # HTTP 公开 API 接口(含 executeDownload)
            │   └── networks_messaging_api.dart   # WebSocket 公开 API 接口(含 updateToken / sendBytes)
            └── wiring/
                ├── api_config.dart               # HTTP 配置(baseURL / token / 回调 / 加密 / 重试 / 主动刷新)
                ├── socket_config.dart            # WebSocket 配置(心跳 / 重连 / 加密钩子 / 压缩)
                ├── network_callbacks.dart        # 回调类型定义(认证 / 加密 / 业务错误 / 下载进度 / WS 加密)
                ├── networks_sdk_core.dart
                ├── networks_sdk_api_impl.dart
                ├── networks_messaging_api_impl.dart
                └── networks_sdk_wiring.dart      # 工厂:build() / buildMessagingApi()
SDK 与 App 层边界
职责SDK (networks_sdk)App 层 (im_app)
Dio 管理ApiClient 内部创建管理通过 NetworksSdkWiring.build(config:) 创建
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)。本项目 disconnectInBackground=false,所有平台后台保活、心跳不停
WebSocket 消息解析JSON.decode → Stream 输出App 层按 type 过滤 + DTO 解析
Riverpod无依赖Provider 包装 NetworksSdkApi / 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

关键规则

傻瓜式教程:从零开始定义并发送一个接口

前置条件(只需做一次)

打开一个独立终端窗口,在项目根目录执行以下命令并保持运行,不要关闭

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 中调用 NetworksSdkApi,转为 Domain Entity

在哪写lib/data/repositories/auth_repository_impl.dart

做什么:调 NetworksSdkApi.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。

import 'package:networks_sdk/networks_sdk.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../remote/login_request.dart';

class AuthRepositoryImpl implements AuthRepository {
  final NetworksSdkApi _client;                   // ← 注入 Facade 接口
  final void Function(String?) _onTokenUpdate;    // ← 回调,由 Provider 层组合

  AuthRepositoryImpl({
    required NetworksSdkApi client,
    required void Function(String?) onTokenUpdate,
  })  : _client = client,
        _onTokenUpdate = onTokenUpdate;

  @override
  Future<User> login({
    required String email,
    required String password,
  }) async {
    // 1. 调 NetworksSdkApi,构造请求 → 发 HTTP → 自动解码 → 返回 DTO
    final LoginData? loginData = await _client.executeRequest(
      LoginRequest(email: email, password: password),
    );

    if (loginData == null) {
      throw Exception('Login failed: empty response');
    }

    // 2. 回调写入 Token(内存 + 持久化由 Provider 层组合)
    _onTokenUpdate(loginData.token);

    // 3. DTO → Domain Entity,返回给上层
    return loginData.toEntity();
  }
}
第 3 步:注册 Provider + 编写 ViewModel

3.1 注册 Provider(DI 装配)

在哪写lib/features/{模块}/di/{模块}_providers.dart

做什么:在 Feature 目录下创建 Provider 文件,注册该模块的 DI 链路(Repository → UseCase 按需)。app/di/ 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 Provider 内聚在 Feature 目录下。

// ── features/auth/di/auth_providers.dart ──

// Repository(注入 Facade 接口 + 回调组合多个 SDK 能力)
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  final apiConfig = ref.read(apiConfigProvider);
  return AuthRepositoryImpl(
    client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
    onTokenUpdate: (token) {
      apiConfig.updateToken(token);               // 内存(networks_sdk)
      // secureStorage.saveToken(token);          // 持久化(storage_sdk,待接入)
    },
  );
});

// UseCase(按需 — 登录有多步编排,所以需要 UseCase)
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
  return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});

新模块示例:添加「消息」模块的 Provider

假设你新建了 MessageRepositoryImpl,需要注册 Provider:

创建 lib/features/chat/di/chat_providers.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/di/network_provider.dart';
import '../../../data/repositories/message_repository_impl.dart';
import '../../../domain/repositories/message_repository.dart';

// ── Repository ──
final messageRepositoryProvider = Provider<MessageRepository>((ref) {
  return MessageRepositoryImpl(
    client: ref.read(networkSdkApiProvider),    // 注入 Facade 接口
  );
});

// 大多数模块只需到 Repository 这一层,ViewModel 直接调 Repository 即可。
// 如需 UseCase(多步编排、跨模块协调),参考 auth_providers.dart 中的 loginUseCaseProvider。

原则app/di/ 只放 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路(Repository → UseCase 按需)内聚在 features/{模块}/di/{模块}_providers.dart 中。

3.2 编写 ViewModel

在哪写lib/features/auth/presentation/login_view_model.dart

常规写法:ViewModel 直接调 Repository

@riverpod
class LoginViewModel extends _$LoginViewModel {
  @override
  LoginState build() => const LoginState();

  Future<void> login(String email, String password) async {
    state = state.copyWith(isLoading: true);

    try {
      // 直接调 Repository
      final user = await ref.read(authRepositoryProvider).login(
        email: email,
        password: password,
      );

      state = state.copyWith(user: user, isLoading: false);
    } on ApiError catch (e) {
      state = state.copyWith(error: e.displayMessage, isLoading: false);
    }
  }
}

进阶写法:有 UseCase 时(如登录需格式校验 + 多步编排)

@riverpod
class LoginViewModel extends _$LoginViewModel {
  @override
  LoginState build() => const LoginState();

  Future<void> login(String email, String password) async {
    state = state.copyWith(isLoading: true);

    try {
      // 通过 UseCase 调用(格式校验 → 登录 → 返回 User)
      final user = await ref.read(loginUseCaseProvider).execute(
        email: email,
        password: password,
      );

      state = state.copyWith(user: user, isLoading: false);
    } on FormatException catch (e) {
      // 格式校验失败(UseCase 层抛出)
      state = state.copyWith(error: e.message, isLoading: false);
    } on ApiError catch (e) {
      // 网络错误统一处理
      state = state.copyWith(error: e.displayMessage, isLoading: false);
    }
  }
}

View 层只需 ref.watch(loginViewModelProvider),状态变化时自动刷新 UI。

完整数据流(从用户点击按钮到 UI 刷新)

常规路径:ViewModel → Repository(大多数场景)

用户点击按钮
  → View: vm.doSomething(...)
    → ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
      → RepositoryImpl.doSomething()                               // data/repositories/
        → _client.executeRequest(XxxRequest)                       // 调 NetworksSdkApi
          → 自动注入 header → HTTP 请求 → 自动解码 → DTO
        → dto.toEntity() → Domain Entity
    ← state = state.copyWith(...)                                  // 更新状态
  ← View: ref.watch → 自动 rebuild                                // 自动刷新

进阶路径:ViewModel → UseCase → Repository(登录等复杂场景)

用户点击「登录」按钮
  → View: vm.login(email, password)
    → ViewModel: ref.read(loginUseCaseProvider).execute(...)
      → LoginUseCase: 格式校验(邮箱 + 密码)                      // features/auth/usecases/
      → LoginUseCase: authRepository.login(...)
        → AuthRepositoryImpl.login()                               // data/repositories/
          → _client.executeRequest(LoginRequest)                   // 调 NetworksSdkApi
            → Auth → Encryption → Dio.request → Retry → Logging // 拦截器链自动处理
          ← request.decodeResponse → LoginData.fromJson           // 自动解码
        ← LoginData(DTO)
        → onTokenUpdate(token)                                    // 回调:内存写入 + 持久化
        ← loginData.toEntity() → User(Domain Entity)
      ← User
    ← state = state.copyWith(user: user)                         // 更新状态
  ← View: ref.watch → 自动 rebuild → UI 显示用户信息              // 自动刷新

你只写了:接口定义文件 + Repository 一个方法 + ViewModel 一个方法。
网络请求、header 注入、token 管理、响应解码、错误处理、JSON 序列化 —— 全部自动完成,你不需要写任何一行。

再来一个:发送消息接口(POST /chat/send-message)

后端接口文档:

POST /chat/send-message
请求体:{ "chat_id": "xxx", "content": "hello" }
响应体:{ "code": 0, "data": { "message_id": "456", "timestamp": 1700000000 } }

你只需创建一个文件lib/data/remote/send_message_request.dart,然后在 Repository 中调用即可。

import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';

part 'send_message_request.g.dart';

// ── Response DTO ──

@JsonSerializable()
class SendMessageData {
  @JsonKey(name: 'message_id')
  final String messageId;
  final int timestamp;

  const SendMessageData({required this.messageId, required this.timestamp});
  factory SendMessageData.fromJson(Map<String, dynamic> json) =>
      _$SendMessageDataFromJson(json);
}

// ── Request ──

@ApiRequest(
  path: ApiPaths.chatSendMessage,           // 路径常量,定义在 api_paths.dart
  method: HttpMethod.post,                 // HTTP 方法
  responseType: SendMessageData,           // 响应类型
  // requestType 不写,默认 ApiRequestType.request(会携带 Token)
)
@JsonSerializable()
class SendMessageRequest extends ApiRequestable<SendMessageData>
    with _$SendMessageRequestApi {
  @JsonKey(name: 'chat_id')               // JSON 字段名和 Dart 字段名不一样时用 @JsonKey
  final String chatId;
  final String content;

  SendMessageRequest({required this.chatId, required this.content});
  @override
  Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
}

保存 → 自动生成 → 然后在 Repository 中调 NetworksSdkApi 就完了:

// 在 MessageRepositoryImpl 中添加
Future<SendMessageData?> sendMessage({
  required String chatId,
  required String content,
}) {
  return _client.executeRequest(
    SendMessageRequest(chatId: chatId, content: content),
  );
}
再来一个:获取用户资料(GET /user/profile)
GET /user/profile          ← 无 query 参数,靠 Authorization token 标识当前用户
响应体:{ "code": 0, "data": { "user_id": "123", "email": "tom@example.com", "nickname": "Tom", "avatar": "https://..." } }
// lib/data/remote/get_profile_request.dart

import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';

part 'get_profile_request.g.dart';

@JsonSerializable()
class ProfileData {
  @JsonKey(name: 'user_id')
  final String userId;
  final String email;
  final String? nickname;
  final String? avatar;

  const ProfileData({required this.userId, required this.email, this.nickname, this.avatar});
  factory ProfileData.fromJson(Map<String, dynamic> json) =>
      _$ProfileDataFromJson(json);

  User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
}

@ApiRequest(
  path: ApiPaths.userProfile,
  method: HttpMethod.get,                  // ← GET 请求,toJson() 结果作为 query string
  responseType: ProfileData,
)
@JsonSerializable()
class GetProfileRequest extends ApiRequestable<ProfileData>
    with _$GetProfileRequestApi {
  GetProfileRequest();  // 无参数 — token 标识当前用户,无需显式传 user_id

  @override
  Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
}
无响应数据的接口(POST /auth/logout)

有些接口不返回 data 字段,只有 {"code": 0, "message": "ok"}。这种情况用 ApiRequestable<void>

// lib/data/remote/logout_request.dart

import 'package:networks_sdk/networks_sdk.dart';

// 无 Response DTO → 泛型写 void
// 不需要 @ApiRequest 注解 → 直接实现 ApiRequestable(最简写法)
class LogoutRequest extends ApiRequestable<void> {
  @override
  String get path => ApiPaths.authLogout;
  @override
  HttpMethod get method => HttpMethod.post;
  @override
  Map<String, dynamic> toJson() => {};  // 无请求体
}

Repository 调用时直接 await

Future<void> logout() async {
  await _client.executeRequest(LogoutRequest());  // 返回 null,直接 await 即可
}
文件上传 — 两种模式

上传与普通请求的核心区别:

对比项普通请求Upload 请求
数据来源toJson() → JSON 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'); },
  );
});

/// Networks SDK API Provider(全局单例,Facade 接口)
/// 内部自动挂载 AuthInterceptor / EncryptionInterceptor / RetryInterceptor / LoggingInterceptor
final networkSdkApiProvider = Provider<NetworksSdkApi>((ref) {
  final config = ref.read(apiConfigProvider);
  return NetworksSdkWiring.build(config: config);
});
DI 装配总览
app/di/                                ← 手动装配:SDK 基础设施
└── network_provider.dart              → apiConfigProvider + networkSdkApiProvider

features/{模块}/di/                    ← 手动装配:业务模块 DI 链路(Repository → UseCase 按需)
├── auth/di/auth_providers.dart        → authRepositoryProvider
│                                        → loginUseCaseProvider(按需)
├── chat/di/chat_providers.dart        → messageRepositoryProvider
│                                        → sendMessageUseCaseProvider(按需)
└── ...                                   (需要时才创建,不提前占位)

features/{模块}/presentation/          ← @riverpod 自动生成:ViewModel Provider
└── login_view_model.dart              → loginViewModelProvider(.g.dart 自动生成)

di/ 目录的定位:只放需要手动装配的 Provider(构造注入、回调组合等)。ViewModel Provider 由 @riverpod 注解自动生成(写在 presentation/ 下),不在 di/ 中。

最小化原则app/di/ 只提供 SDK 能力(ApiConfig / NetworksSdkApi),不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 features/{模块}/di/{模块}_providers.dart 中,需要时才创建。

SDK 间解耦:回调注入模式

Repository 不直接依赖 SDK 类型(如 ApiConfig)。需要 SDK 能力(如写 Token)时,通过回调注入,由 Provider 层组合多个 SDK:

// features/auth/di/auth_providers.dart — App 层是唯一知道两个 SDK 的地方
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  final apiConfig = ref.read(apiConfigProvider);
  // final secureStorage = ref.read(secureStorageProvider);  // storage_sdk(待接入)

  return AuthRepositoryImpl(
    client: ref.read(networkSdkApiProvider),       // 注入 Facade 接口
    onTokenUpdate: (token) {
      apiConfig.updateToken(token);               // 内存(networks_sdk)
      // secureStorage.saveToken(token);          // 持久化(storage_sdk,待接入)
    },
  );
});

好处networks_sdk 不知道 storage_sdk 的存在,AuthRepositoryImpl 也不依赖任何 SDK 类型——只接收一个 void Function(String?) 回调。各 SDK 保持独立,App 层负责组合。

错误处理

ApiError 是 Freezed 联合类型,覆盖所有网络错误场景。在 ViewModel 的 catch 中使用:

try {
  final user = await ref.read(loginUseCaseProvider).execute(...);
} on ApiError catch (e) {
  // 方式 A:用 displayMessage 一行搞定(ApiError 扩展方法,已内置中文提示)
  showToast(e.displayMessage);

  // 方式 B:精细处理每种错误
  e.when(
    noNetworkConnection: () => showToast('无网络连接'),
    timeout: ()            => showToast('请求超时,请重试'),
    networkError: (msg)    => showToast('网络错误: $msg'),
    decodingError: (msg)   => showToast('数据解析失败'),
    apiError: (code, msg)  => showToast('服务端错误[$code]: $msg'),
    cancelled: ()          => {}, // 用户主动取消,通常不提示
    unknown: (msg)         => showToast('未知错误'),
  );
}
代码生成(新人必读)

@ApiRequest@JsonSerializable 共享同一个 .g.dart 文件(SharedPartBuilder),无需额外配置。

开发流程(3 步)

第 0 步:启动监听(整个开发期间只需执行一次,在独立终端窗口常驻)

# ⚠️ 强制要求:开发期间必须常驻此命令
# 在项目根目录打开一个独立终端窗口,执行:
melos run gen:watch

启动后,每次保存 .dart 文件都会自动重新生成 .g.dart,无需手动操作。

第 1 步:手写源文件

// data/remote/login_request.dart

import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';

part 'login_request.g.dart';  // ← 必须写,指向即将生成的文件

// ── Response DTO ──
@JsonSerializable()
class LoginData {
  final String token;
  final String email;
  const LoginData({required this.token, required this.email});

  // ↓ 此时 _$LoginDataFromJson 还不存在,IDE 会报红,正常!
  factory LoginData.fromJson(Map<String, dynamic> json) =>
      _$LoginDataFromJson(json);
  Map<String, dynamic> toJson() => _$LoginDataToJson(this);
}

// ── Request ──
@ApiRequest(
  path: ApiPaths.authLogin,
  method: HttpMethod.post,
  responseType: LoginData,
  requestType: ApiRequestType.login,
)
@JsonSerializable()
class LoginRequest extends ApiRequestable<LoginData>
    with _$LoginRequestApi {                // ← 短暂报红,保存后自动消失
  final String email;
  final String password;

  LoginRequest({required this.email, required this.password});

  @override
  Map<String, dynamic> toJson() => _$LoginRequestToJson(this);  // ← 短暂报红,保存后自动消失
}

第 2 步:保存文件 → 自动生成

保存后,后台的 melos run gen:watch 自动检测到文件变化,生成 login_request.g.dart
所有红线自动消失,无需任何手动操作。

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

注解生成的符号示例
@JsonSerializable()_$类名FromJson()_$LoginDataFromJson(json)
@JsonSerializable()_$类名ToJson()_$LoginDataToJson(this)
@ApiRequest(...)_$类名Api(mixin)_$LoginRequestApi

规则固定,先写引用再保存,watch 模式下几秒后红线自动消失。如果红线没消失,检查 watch 终端是否在运行。

常见问题

  • 忘了启动 watch:保存后红线不消失 → 检查终端是否有 melos run gen:watch 在运行
  • 生成报错melos run gen 重新全量生成
  • .g.dart 冲突:多人协作时 .g.dart 冲突直接删除后重新生成即可,不要手动合并
  • 新增依赖后:先 dart pub get,再重启 watch

Storage SDK(packages/storage_sdk/)

纯基础设施 SDK,不感知业务表结构。遵循 Facade + Wiring 模式,结构同 cipher_guard_sdk。

职责边界:

使用方式:

// 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 用于推送通知解密):

加解密性能优化

encryption_flutter_service.dart 针对 IM 高频加解密场景做了四项优化:

优化项 方案 效果
RSA 密钥生成异步化 generateRsaKeyPairAsync 使用 Isolate.run() 在独立线程生成 主线程零阻塞,不卡 UI(2048-bit 约 200-500ms)
派生密钥 LRU 缓存 _derivedKeyCache(Map,上限 64 条),缓存键 = sessionKey:round:mode,满时淘汰最早条目 同一 round 的加解密只算一次 KDF,后续直接命中缓存
Random.secure() 复用 静态 _secureRandom 单例,所有 IV / 随机数生成共用 避免每次 Random.secure() 构造开销
KDF 双模式 KdfMode.md5(默认,兼容既有数据)和 KdfMode.pbkdf2(PBKDF2-HMAC-SHA256,可配迭代次数) 默认快速兼容,可选安全增强(防暴力破解)

构造时可配置 KDF 模式和 PBKDF2 迭代次数:

EncryptionFlutterService(
  kdfMode: KdfMode.md5,        // 默认,兼容既有数据
  pbkdf2Iterations: 10000,     // PBKDF2 模式下的迭代次数
)

clearDerivedKeyCache() 可在 session key 轮换时手动清空缓存。

7.4 多语言国际化(packages/l10n_sdk/)

已提取为独立 Package,被 core/ui 和 Feature 层单向引用(foundation 不依赖它)。

核心策略:远端动态下载,一劳永逸

翻译文案不随包发布,App 启动后自动从远端 URL 拉取最新版本并写入共享空间,Dart 层和原生层(Android / iOS)均从共享空间读取。后续只需在多语言后台追加/修改文案,无需发版,立即生效。

启动流程

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

兜底机制

模块职责

为什么独立 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:

第二层:基础组件(core/ui/components/)

原子级 Widget,只依赖第一层 base,不含任何业务逻辑

第三层:业务组合组件(core/ui/composites/)

由 base + components 组合而成的高阶 Widget,封装通用业务交互模式:

依赖方向: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. 依赖约束:SDK 之间不能相互依赖(除非明确声明)
  3. 职责约束:SDK 只提供纯技术能力,不包含业务逻辑
  4. 版本管理:使用 Melos 统一管理版本和依赖
  5. 独立性:每个 SDK 可独立测试、发布

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



扩展性设计

8.1 新增 Feature

添加新功能的标准流程:

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

8.2 标准 Feature 结构模板

每个新 Feature 都应遵循以下标准结构:

两档模板选择指南

复杂度适用场景必须有可选添加
简单 search、settings、profile 等 view/ + presentation/(vm + state) di/(需要自定义 Provider 时)
标准 chat、call、auth 等复杂功能 view/ + presentation/ + di/ usecases/(多步编排、跨模块协调时按需添加)

简单模板(search / settings / profile 类)

features/[feature]/
├── view/
│   ├── [feature]_page.dart
│   └── widgets/
└── presentation/
    ├── [feature]_view_model.dart        # ViewModel(直接调 Repository)
    ├── [feature]_view_model.g.dart      # 代码生成
    ├── [feature]_state.dart             # State(@freezed)
    └── [feature]_state.freezed.dart     # 代码生成

标准模板(chat / call / auth 类)

features/[feature]/
├── di/
│   └── [feature]_providers.dart         # DI 装配(Repository → UseCase 按需)
├── view/
│   ├── [feature]_page.dart
│   └── widgets/
├── presentation/
│   ├── [feature]_view_model.dart
│   ├── [feature]_view_model.g.dart
│   ├── [feature]_state.dart
│   └── [feature]_state.freezed.dart
└── usecases/                            # 按需 — 有多步编排时才添加
    └── [action]_usecase.dart

Domain Entity 说明:共享实体(Message、Contact、User 等)统一放在全局 domain/entities/,不在 feature 内部定义。

完整示例:创建 Profile Feature(简单模板)

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 NetworksSdkApi _client;

  ProfileRepositoryImpl({required NetworksSdkApi client})
      : _client = client;

  @override
  Future<Profile> getProfile() async {
    final data = await _client.executeRequest(GetProfileRequest());
    return data!.toEntity();
  }

  @override
  Future<void> updateProfile(Profile profile) async {
    // 实现逻辑
  }
}

步骤 6:在 Feature 目录下注册模块 Provider

创建 features/profile/di/profile_providers.dart,注册 DI 链路:

// features/profile/di/profile_providers.dart

import '../../../app/di/network_provider.dart';

// ── Repository ──
final profileRepositoryProvider = Provider<ProfileRepository>((ref) {
  return ProfileRepositoryImpl(
    client: ref.read(networkSdkApiProvider),    // 注入 Facade 接口
  );
});

// Profile 是简单 CRUD,不需要 UseCase。
// ViewModel 通过 @riverpod 注解自动生成 Provider,无需额外注册。

说明:Profile 属于简单模板,ViewModel 直接调 Repository,无需 UseCase 中间层。app/di/ 只提供 SDK 基础设施(ApiConfig / NetworksSdkApi),业务模块的 DI 链路内聚在 Feature 目录下。

Feature 结构图示

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 替换底层实现

由于依赖倒置原则,可以轻松替换底层实现:

8.4 跨平台扩展



项目配置

10.1 pubspec.yaml 依赖

核心依赖

name: im_app
description: IM Application with Clean Architecture
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # 状态管理 - Riverpod
  flutter_riverpod: ^2.4.0
  riverpod_annotation: ^2.3.0

  # 不可变状态 - Freezed
  freezed_annotation: ^2.4.1

  # JSON 序列化
  json_annotation: ^4.8.1

  # 依赖注入(可选,Riverpod 已包含依赖管理)
  # get_it: ^7.6.0
  # injectable: ^2.3.0

  # 路由导航
  go_router: ^12.0.0

  # 网络请求
  dio: ^5.4.0
  web_socket_channel: ^2.4.0

  # 本地存储
  drift: ^2.15.0
  sqlite3_flutter_libs: ^0.5.0
  flutter_secure_storage: ^9.0.0
  shared_preferences: ^2.2.0

  # 加密
  encrypt: ^5.0.3
  crypto: ^3.0.3

  # 媒体处理
  image_picker: ^1.0.4
  video_player: ^2.8.0
  cached_network_image: ^3.3.0

  # RTC
  agora_rtc_engine: ^6.3.0
  # 或者 flutter_webrtc: ^0.9.0

  # 推送通知
  firebase_messaging: ^14.7.0
  flutter_local_notifications: ^16.3.0

  # Protocol Buffers
  protobuf: ^3.1.0

  # 工具库
  equatable: ^2.0.5
  dartz: ^0.10.1
  intl: ^0.18.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 代码生成
  build_runner: ^2.4.6
  riverpod_generator: ^2.3.0
  freezed: ^2.4.5
  json_serializable: ^6.7.1
  drift_dev: ^2.15.0

  # 代码检查
  flutter_lints: ^3.0.0
  very_good_analysis: ^5.1.0

  # 测试
  mocktail: ^1.0.1
  integration_test:
    sdk: flutter

依赖说明

类别 包名 用途
状态管理 flutter_riverpod Riverpod 核心库
riverpod_annotation Riverpod 注解,用于代码生成
不可变状态 freezed_annotation Freezed 注解,生成不可变类
代码生成 build_runner Dart 代码生成工具
riverpod_generator Riverpod Provider 代码生成
freezed Freezed 代码生成
json_serializable JSON 序列化代码生成
网络 dio HTTP 客户端
web_socket_channel WebSocket 通信
存储 drift 类型安全的响应式数据库(基于 SQLite)
flutter_secure_storage 安全存储(加密)
测试 mocktail Mock 测试工具

10.2 代码生成命令

项目使用 Melos 统一管理,所有代码生成命令通过 melos run 执行,会自动作用于所有依赖 build_runner 的 Package。

一次性生成

用于 CI 流水线,或首次 clone 后手动触发一次:

melos run gen

监听模式(开发期间必开)

开发时需要在一个独立的 Terminal 窗口中启动,全程保持运行。每次保存 .dart 文件后,build_runner 会自动检测变化并重新生成对应的 .g.dart / .freezed.dart 文件,无需手动执行。

# 在独立 Terminal 窗口执行,不要关闭
melos run gen:watch

⚠️ 若忘记开启,修改了 @freezed / @JsonSerializable 等注解后不会自动生成,编译时会报找不到对应文件的错误。

底层等价命令(参考)

Melos 实际代为执行的命令,无需手动调用:

dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch --delete-conflicting-outputs

10.3 环境配置与启动入口

设计思路

采用 单一配置文件 + CI 脚本写入 的方式管理多环境配置:

config/config.json(提交到 Git,默认 dev)

{
  "IS_DEV": true,
  "API_BASE_URL": "https://dev-api.example.com"
}

后续新增字段(WebSocket 地址、Sentry DSN、第三方 SDK Key 等)直接在此文件加 Key 即可,无需改启动逻辑。

core/foundation/config.dart

// 编译期从 --dart-define-from-file=config/config.json 注入
// CI 打包时脚本修改 config.json 写入线上值,本地开发保持默认(IS_DEV=true)
const _kIsDebug = bool.fromEnvironment('IS_DEV', defaultValue: true);
const _kApiBaseUrl = String.fromEnvironment(
  'API_BASE_URL',
  defaultValue: 'https://dev-api.example.com',
);

class AppConfig {
  const AppConfig({
    required this.isDebug,
    required this.apiBaseUrl,
  });

  /// 根据注入的编译期常量构建配置,main.dart 唯一入口
  static AppConfig get current => const AppConfig(
        isDebug: _kIsDebug,
        apiBaseUrl: _kApiBaseUrl,
      );

  final bool isDebug;
  final String apiBaseUrl;

  bool get isProd => !isDebug;
}

app/bootstrap.dart

void bootstrap(AppConfig config) {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(IMApp(config: config));
}

main.dart

void main() {
  bootstrap(AppConfig.current);
}

Android Studio / VSCode 运行配置

在 Android Studio 的 Run/Debug Configurations → Additional run args 中配置:

配置名Additional run args说明
im-debug--dart-define-from-file=config/config.json --debug本地开发调试
im-release--dart-define-from-file=config/config.json --release本地 Release 模式验证

路径 config/config.json 相对于 Flutter 模块根目录(apps/im_app/)。配置已保存在 .idea/runConfigurations/ 中,clone 后 Android Studio 可直接使用。

打包脚本

每个平台均有独立的打包脚本,统一放在 scripts/ 目录下,并通过 melos 命令调用。所有脚本均启用 --split-debug-info + --obfuscate 以减少产物体积。

平台脚本melos 命令用途产物
Android scripts/build_android.sh melos run build:android:apk 本地测试 / 内部分发 build/app/outputs/flutter-apk/app-release.apk
melos run build:android:aab Google Play 上架 build/app/outputs/bundle/release/app-release.aab
iOS scripts/build_ios.sh melos run build:ios App Store / 内部分发 build/ios/ipa/im_app.ipa
macOS scripts/build_macos.sh melos run build:macos build/macos/Build/Products/Release/im_app.app
Windows scripts/build_windows.sh melos run build:windows build/windows/x64/runner/Release/

melos 调用方式

melos run build:android:apk       # APK(本地测试)
melos run build:android:aab       # AAB(Google Play)
melos run build:ios
melos run build:macos
melos run build:windows

# prod 打包(需设置环境变量)
PROD_API_BASE_URL=https://api.example.com melos run build:android:apk -- apk prod
PROD_API_BASE_URL=https://api.example.com melos run build:android:aab -- aab prod
PROD_API_BASE_URL=https://api.example.com melos run build:ios -- prod

体积优化说明

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 不可合并

触发规则

打包策略

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

预留 CI 能力

能力触发时机状态说明
AI 代码 Review PR 提交 / 更新时 🔜 预留 对每个 PR 的 diff 调用 AI 接口,自动输出可读性、架构合规性、潜在问题等 Review 意见,以 PR 评论形式呈现

分支保护建议(Gitea → Settings → Branches)



第五部分:数据流转示例

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/
NetworksSdkApi / SocketClient participant WS as WebSocket Server UI->>VM: 1. 用户点击发送按钮 VM->>UC: 2. 调用 SendMessageUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Data 层实现接口 RepoImpl->>LocalDS: 5. 先保存到本地数据库 LocalDS-->>RepoImpl: 6. 本地保存成功(设置状态:发送中) RepoImpl->>SDK: 7. 直接调 SDK 发送 SDK->>WS: 8. 发送消息到服务器 WS-->>SDK: 9. 服务器确认收到 SDK-->>RepoImpl: 10. 返回发送结果 RepoImpl->>LocalDS: 11. 更新本地消息状态(已发送) LocalDS-->>RepoImpl: 12. 更新成功 RepoImpl-->>UC: 13. 返回 Message Entity UC-->>VM: 14. 返回结果 VM-->>UI: 15. 更新 UI 状态 UI->>UI: 16. 显示消息已发送 Note over UI,WS: Feature 垂直切片:chat Feature 的完整数据流

9.2 流程说明

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

完整代码示例(Riverpod 实现)

1. UI 层(ConsumerWidget)

// features/chat/view/chat_page.dart
class ChatPage extends ConsumerWidget {
  const ChatPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(chatViewModelProvider);
    final viewModel = ref.read(chatViewModelProvider.notifier);

    return Scaffold(
      body: ChatInputArea(
        onSend: (content) => viewModel.sendMessage(content),
      ),
    );
  }
}

2. Presentation 层(StateNotifier)

// features/chat/presentation/chat_view_model.dart
class ChatViewModel extends StateNotifier<ChatState> {
  ChatViewModel(this._sendMessageUseCase) : super(const ChatState());

  final SendMessageUseCase _sendMessageUseCase;

  Future<void> sendMessage(String content) async {
    state = state.copyWith(isLoading: true);
    try {
      await _sendMessageUseCase(content);
    } finally {
      state = state.copyWith(isLoading: false);
    }
  }
}

// Provider
final chatViewModelProvider =
    StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
  return ChatViewModel(ref.watch(sendMessageUseCaseProvider));
});

3. Domain 层(UseCase + Provider)

// features/chat/usecases/send_message_usecase.dart
class SendMessageUseCase {
  final MessageRepository _repository;

  SendMessageUseCase(this._repository);

  Future<Message> call(String content) {
    return _repository.sendMessage(content);
  }
}

// Provider
@riverpod
SendMessageUseCase sendMessageUseCase(SendMessageUseCaseRef ref) {
  return SendMessageUseCase(ref.watch(messageRepositoryProvider));
}

// domain/repositories/message_repository.dart
abstract class MessageRepository {
  Future<Message> sendMessage(String content);
  Future<List<Message>> getMessages(String chatId);
}

4. Data 层(Repository 实现 + Providers)

// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
  final MessageLocalDataSource _localDS;
  final NetworksSdkApi _client;      // 注入 Facade 接口

  MessageRepositoryImpl(this._localDS, this._client);

  @override
  Future<Message> sendMessage(String content) async {
    // 1. 先保存到本地
    final localMessage = await _localDS.saveMessage(content, status: 'sending');

    // 2. 直接调 SDK 发送到服务器
    try {
      final serverMessage = await _client.executeRequest(
        SendMessageRequest(chatId: localMessage.chatId, content: content),
      );

      // 3. 更新本地状态
      await _localDS.updateMessageStatus(localMessage.id, 'sent');

      return serverMessage!.toEntity();
    } catch (e) {
      await _localDS.updateMessageStatus(localMessage.id, 'failed');
      rethrow;
    }
  }
}

// Provider
@riverpod
MessageRepository messageRepository(MessageRepositoryRef ref) {
  return MessageRepositoryImpl(
    ref.watch(messageLocalDataSourceProvider),
    ref.watch(networkSdkApiProvider),     // 注入 Facade 接口
  );
}

5. Local DataSource + Provider

// data/local/message_local_ds.dart
class MessageLocalDataSource {
  final AppDatabase _db;

  Future<MessageDTO> saveMessage(String content, {required String status}) {
    // Drift 操作
  }
}

// Provider
@riverpod
MessageLocalDataSource messageLocalDataSource(MessageLocalDataSourceRef ref) {
  return MessageLocalDataSourceImpl(ref.watch(databaseProvider));
}

9.3 加载会话列表流程

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/
NetworksSdkApi UI->>VM: 1. 页面初始化 VM->>UC: 2. 调用 LoadChatListUseCase UC->>Repo: 3. 调用 Repository 接口 Repo->>RepoImpl: 4. Repository 实现 RepoImpl->>Cache: 5. 检查缓存 alt 缓存命中 Cache-->>RepoImpl: 6a. 返回缓存数据 else 缓存未命中 RepoImpl->>LocalDS: 6b. 读取本地数据库 LocalDS-->>RepoImpl: 7. 返回本地数据 RepoImpl->>SDK: 8. 调 NetworksSdkApi 请求远程数据 SDK-->>RepoImpl: 9. 返回最新数据 RepoImpl->>LocalDS: 10. 更新本地数据库 RepoImpl->>Cache: 11. 更新缓存 end RepoImpl-->>UC: 12. 返回 Chat Entity 列表 UC-->>VM: 13. 返回结果 VM-->>UI: 14. 更新 UI 状态 UI->>UI: 15. 显示会话列表 Note over UI,SDK: 缓存优先策略:缓存 → 本地 → 远程

9.4 跨 Feature 交互

不同 Feature 之间通过共享的 Repository 接口交互:

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

关键原则

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
转换层 Data DTO ↔ Entity 数据转换 Mapper、Converter
协议层 Core 封装通信协议,屏蔽底层细节 APIRequestable、WebSocketProtocol
策略层 Core/Data 封装算法和策略,易于替换 CacheStrategy、RetryStrategy

中间层实践示例

// 1. Repository 接口层(抽象层)
abstract class ChatRepository {
  Future<List<Message>> getMessages(String chatId);
  Future<void> sendMessage(Message message);
}

// 2. Repository 实现层(注入 Facade 接口)
class ChatRepositoryImpl implements ChatRepository {
  final NetworksSdkApi client;
  final LocalDataSource localDataSource;
  final MessageMapper mapper;

  ChatRepositoryImpl({
    required this.client,
    required this.localDataSource,
    required this.mapper,
  });

  @override
  Future<List<Message>> getMessages(String chatId) async {
    // 先从本地获取
    final localDTOs = await localDataSource.getMessages(chatId);
    if (localDTOs.isNotEmpty) {
      return localDTOs.map(mapper.toEntity).toList();
    }

    // 调 NetworksSdkApi 从远程获取
    final response = await client.executeRequest(
      GetMessagesRequest(chatId: chatId),
    );

    // 保存到本地
    await localDataSource.saveMessages(response?.messages ?? []);

    // 转换为 Entity
    return (response?.messages ?? []).map(mapper.toEntity).toList();
  }
}

// 4. 转换层(Mapper)
class MessageMapper {
  Message toEntity(MessageDTO dto) {
    return Message(
      id: dto.id,
      content: dto.content,
      senderId: dto.senderId,
      timestamp: DateTime.parse(dto.timestamp),
    );
  }

  MessageDTO toDTO(Message entity) {
    return MessageDTO(
      id: entity.id,
      content: entity.content,
      senderId: entity.senderId,
      timestamp: entity.timestamp.toIso8601String(),
    );
  }
}

// 5. 策略层
abstract class CacheStrategy {
  bool shouldCache(String key);
  Duration getCacheDuration(String key);
}

class MessageCacheStrategy implements CacheStrategy {
  @override
  bool shouldCache(String key) {
    // 最近 100 条消息缓存
    return true;
  }

  @override
  Duration getCacheDuration(String key) {
    return Duration(hours: 24);
  }
}

中间层的价值

  • 解耦:各层通过接口通信,修改实现不影响调用方
  • 可测试:每层可独立测试,易于 Mock
  • 可替换:底层实现可随时替换(如换数据库、换 API)
  • 可扩展:新增功能只需新增实现,不修改接口
  • 可维护:职责清晰,问题定位快速

5.4 系统能力划分

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

能力划分架构

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/gRPC NetworkSDK 完全通用
数据存储 Drift/SharedPreferences/SecureStorage StorageSDK 完全通用
端对端加密 RSA/AES 双层加密 + Native 密钥同步 CipherGuardSDK 完全通用
媒体处理 图片/视频/音频压缩 MediaSDK 高度通用
音视频通话 WebRTC/Agora RTCSDK 较为通用
推送通知 FCM/APNs/本地通知 NotificationSDK 高度通用
推送解密 iOS App Group 密钥同步(Notification Extension) CipherGuardSDK 高度通用
协议序列化 Protocol Buffers/JSON ProtocolSDK 完全通用

业务能力层

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

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

快速响应机制

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

/// 快速响应管理器
class FastResponseManager {
  /// 智能缓存 - 预测用户行为并缓存
  Future<void> smartCache() async {
    // 预缓存最近联系人的头像
    final recentContacts = await _getRecentContacts();
    for (final contact in recentContacts) {
      await ImageCache.precache(contact.avatar);
    }

    // 预缓存最近会话的最后几条消息
    final recentChats = await _getRecentChats();
    for (final chat in recentChats) {
      await MessageCache.precache(chat.id, limit: 20);
    }
  }

  /// 预加载 - 提前加载可能访问的数据
  Future<void> preload() async {
    // 预加载用户资料
    await UserProfilePreloader.preload();

    // 预加载表情包
    await EmojiPreloader.preload();

    // 预加载常用设置
    await SettingsPreloader.preload();
  }

  /// 性能优化 - 优化关键路径
  Future<void> optimize() async {
    // 延迟加载非关键资源
    await LazyLoader.load();

    // 分帧渲染大列表
    await ListOptimizer.optimize();

    // 图片懒加载
    await ImageLazyLoader.setup();
  }
}

/// 响应时间监控
class ResponseTimeMonitor {
  static void track(String operation, Duration duration) {
    final ms = duration.inMilliseconds;

    // 记录慢操作
    if (ms > 100) {
      Logger.warn('Slow operation: $operation took ${ms}ms');
    }

    // 上报性能数据
    Analytics.track('response_time', {
      'operation': operation,
      'duration_ms': ms,
    });
  }
}

容器化部署?

潜在方向:可考虑通过容器化实现快速部署、弹性扩展、版本管理。

注:容器化部署更多适用于后端服务,对于移动端 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 率 频繁出现 Bug Bug 很少 显著减少
新功能开发 缓慢 快速 明显提速
技术债务 严重 很少 避免重构
团队效率 低(大量救火) 高(专注开发) 显著提升
代码质量 差(难以维护) 优(易于维护) 可持续发展

长期收益体现

初期投入 vs 长期回报

收益来源

长期收益的关键指标

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 级别错误立即上报并触发告警 及时发现严重问题
性能优化 日志写入异步化,不阻塞主线程 不影响用户体验
存储管理 本地日志定期清理,避免占用过多空间 节省存储空间

日志与监控的核心价值

  • 问题定位:通过完整的日志链路,快速定位问题根因
  • 性能优化:实时监控性能指标,及时发现和解决性能瓶颈
  • 用户体验:监控用户行为,优化产品功能和交互
  • 故障预警:实时告警机制,在问题扩散前及时处理
  • 数据驱动:基于监控数据做决策,而非主观猜测

第七部分:总结

架构优势

核心架构设计

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. Feature 垂直切片:每个 Feature 包含 view/presentation/domain 的完整链路
  3. 全局 Repository 接口:所有 Repository 接口定义在 domain/repositories/
  4. 统一 Data 实现:所有 Repository 实现在 data/repositories/
  5. UseCase 单一职责:每个 UseCase 只处理一个业务场景
  6. Riverpod 状态管理:使用 StateNotifier 管理 UI 状态,Providers 管理依赖
  7. 使用代码生成:利用 riverpod_generator 和 freezed 减少样板代码
  8. 使用 Melos 管理依赖:Mono-Repo 保证版本一致性
  9. 编写完整测试:单元测试、集成测试、端到端测试
  10. UI 层使用 ConsumerWidget:通过 ref.watch 监听状态,ref.read 读取 Provider

关键原则

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

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

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

第八部分:UI 设计规范

本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。

8.0 核心约定

全局只有一份

颜色、字体、基础组件、业务弹框、图片、图标——Figma 里每种元素只有一个定义。没有"备用版本",没有"临时副本",不允许两个"差不多一样"的组件并存。

  1. Figma 命名是重中之重:点中任何元素都必须看到抽象名称。六类无例外:
    • 颜色 — 如 primarysurface
    • 字体 — 如 Body/MediumLabel/Small
    • 基础组件 — 如 Button/PrimaryInput/Default,全局只有一个版本
    • 业务弹框 — 如 Dialog/Confirm
    • 图片 — Figma 统一导出,代码侧 AppAssets 注册,不硬编码路径
    • 图标 — 如 sendmore_options,代码侧 AppIcons 调用
  2. 基础组件定稿后不随意改动:需改动时必须先告知研发,评估影响范围,双方同步后再执行
  3. UI 团队自主维护 UI 基建体系:研发照着 Figma 名字封装,名称必须完全一致
  4. 所有元素遵循同一套命名规则:新增任何元素先在 Figma 定名,研发用相同名字注册
图片和组件是重灾区:没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。

8.1 颜色体系

所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。

语义色(随主题变化)

抽象名Figma 名亮色暗色用途
primaryPrimary#2F80ED#5BA3F5主操作、链接、选中态
backgroundBackground#F8F9FA#202124页面底色
surfaceSurface#FFFFFF#3C4043卡片、弹框、输入框
onSurfaceOn Surface#202124#FFFFFFsurface 上的文字
errorError#EB5757错误状态
successSuccess#27AE60成功状态
warningWarning#F2C94C警告状态

灰阶(固定值,不随主题变化)

名称色值名称色值名称色值
white#FFFFFFgray50#F8F9FAgray100#F1F3F4
gray200#E8EAEDgray400#BDC1C6gray600#80868B
gray800#3C4043gray900#202124black#000000

使用原则:需随主题切换 → 用语义色(primarysurface);亮暗保持不变 → 用灰阶固定值。

8.2 字体体系

字体按层级分五档:Display、Headline、Title、Body、Label,每档三个尺寸。Figma 中按 层级/尺寸 格式命名(如 Body/Large),开发用同名变量调用。

Figma 名称字号字重行高字距典型用途
DISPLAY
Display/Large5740064-0.25启动页大标题、空状态
Display/Medium4540052
Display/Small3640044
HEADLINE
Headline/Large3240040页面主标题、导航栏
Headline/Medium2840036
Headline/Small2440032
TITLE
Title/Large2250028会话列表名称、设置项标题
Title/Medium16500240.15卡片标题、列表主行
Title/Small14500200.1
BODY
Body/Large16400240.5聊天气泡、表单输入
Body/Medium14400200.25正文说明、列表副行
Body/Small12400160.4辅助信息、提示文字
LABEL
Label/Large14500200.1按钮文字、Tab 标签、Badge
Label/Medium12500160.5次要标签、徽标文字
Label/Small11500160.5最小粒度标签
语义样式
Section Label136000.5列表分组标题、设置分区
Body/Muted1240016说明文字(灰色,低对比度)
Body/Error1240016表单错误提示(红色)
Label/Muted1250016时间戳、元数据(低对比度)

8.3 组件 — Button

按钮共四种变体,每种有明确使用场景和 Figma 组件名。每个页面上主操作只用一个 Primary。

Figma 组件名用途亮色样式暗色样式状态
Button/Primary主操作(登录、发送、确认),每屏最多一次背景 #2F80ED,白字背景 #5BA3F5,白字默认 / Loading / 禁用(#BDC1C6)
Button/Secondary次要操作(注册、稍后再说),描边样式描边 #2F80ED,蓝字描边 #5BA3F5,蓝字默认 / 禁用
Button/Text辅助链接(忘记密码、查看全部、弹框取消)无背景,蓝字无背景,蓝字默认
Button/Inverse反色按钮(深色背景高亮),支持左侧图标背景 #202124,白字背景 #FFFFFF,黑字默认

8.4 业务弹框 — Dialog

当前封装一种通用确认弹框 Dialog/Confirm,后续新增先在 Figma 以 Dialog/ 前缀命名。

项目说明
结构标题(不超 15 字) + 内容 + 操作区(取消 Text 样式 | 确认 Primary 样式)
可配置标题文字、内容文字、确认/取消标签、点击背景是否可关闭
返回值确认 / 取消 / 关闭(点击背景)

8.5 图标规范

  1. UI 先命名,开发跟随:Figma 确定名称,开发用完全相同的名称封装到 AppIcons
  2. 名称有实际语义:全小写下划线,如 sendadd_contactmore_options。不用拼音,不缩写
  3. 统一用 AppIcons 调用:不允许直接用裸 icon 库变量,替换时改一处全局生效
  4. 同义图标只保留一个:同功能图标在整个产品内只存在一种

新增图标流程:设计师 Figma 确认名称 → 告知开发 → 开发用相同名称在 AppIcons 注册。两端名称必须完全一致。