Files
customer-im-client-dev/Doc/IM_App_架构设计.html
2026-03-06 15:05:53 +08:00

9866 lines
349 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IM App 整体架构设计 - 可视化版</title>
<!-- Mermaid.js CDN -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
themeVariables: {
fontSize: '14px'
}
});
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.8;
color: #333;
background: #f5f7fa;
}
/* 主容器 */
.container {
display: flex;
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 280px;
background: #2c3e50;
color: #ecf0f1;
position: fixed;
height: 100vh;
overflow-y: auto;
padding: 20px 0;
z-index: 1000;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-thumb {
background: #34495e;
border-radius: 3px;
}
.sidebar h2 {
padding: 0 20px 15px;
font-size: 18px;
border-bottom: 1px solid #34495e;
margin-bottom: 10px;
}
.sidebar nav {
padding: 10px 0;
}
.sidebar nav a {
display: block;
padding: 10px 20px;
color: #bdc3c7;
text-decoration: none;
transition: all 0.25s ease;
font-size: 14px;
border-left: 3px solid transparent;
font-weight: 500;
}
.sidebar nav a:hover {
background: #34495e;
color: #fff;
border-left-color: #3498db;
padding-left: 25px;
}
.sidebar nav a.active {
background: #3d2b00;
color: #f39c12;
border-left: 3px solid #f39c12;
font-weight: 700;
}
.sidebar nav a.sub-item {
padding: 8px 20px 8px 35px;
font-size: 13px;
font-weight: 400;
}
.sidebar nav a.sub-item:hover {
padding-left: 40px;
}
.sidebar nav a.sub-item-2 {
padding: 6px 20px 6px 50px;
font-size: 12px;
color: #95a5a6;
font-weight: 300;
}
.sidebar nav a.sub-item-2:hover {
color: #fff;
padding-left: 55px;
}
.sidebar nav a.sub-item-2.active {
color: #5dade2;
}
/* 主内容区 */
.main-content {
margin-left: 280px;
flex: 1;
padding: 40px 60px;
max-width: 1400px;
}
/* 顶部头部 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 60px 40px;
border-radius: 12px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.header h1 {
font-size: 36px;
margin-bottom: 20px;
font-weight: 700;
}
.header blockquote {
background: transparent !important;
border-left: none !important;
padding: 0 !important;
margin: 20px 0 0 0 !important;
color: #ffffff !important;
font-size: 16px;
line-height: 1.8;
}
.header blockquote p {
color: #ffffff !important;
margin: 5px 0;
}
/* 章节标题 */
h2 {
font-size: 28px;
color: #2c3e50;
margin: 50px 0 25px;
padding-bottom: 15px;
border-bottom: 3px solid #667eea;
font-weight: 600;
}
h3 {
font-size: 22px;
color: #34495e;
margin: 35px 0 20px;
font-weight: 600;
}
h4 {
font-size: 18px;
color: #555;
margin: 25px 0 15px;
font-weight: 600;
}
/* 段落 */
p {
margin: 15px 0;
color: #555;
}
/* 列表 */
ul, ol {
margin: 15px 0;
padding-left: 30px;
}
li {
margin: 8px 0;
color: #555;
}
/* 代码块 */
pre {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 20px;
overflow-x: auto;
margin: 20px 0;
font-size: 14px;
}
code {
background: #f1f3f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 13px;
color: #d63384;
}
pre code {
background: none;
padding: 0;
color: #333;
}
/* Mermaid 图表容器 */
.mermaid {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 30px;
margin: 30px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
}
/* 表格 */
table {
width: 100%;
border-collapse: collapse;
margin: 25px 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 15px;
text-align: left;
font-weight: 600;
}
td {
padding: 12px 15px;
border-bottom: 1px solid #e9ecef;
color: #555;
}
tr:hover {
background: #f8f9fa;
}
/* 分隔线 */
hr {
border: none;
border-top: 2px solid #e9ecef;
margin: 40px 0;
}
/* 引用块 */
blockquote {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 15px 20px;
margin: 20px 0;
color: #555;
font-style: italic;
}
/* 链接 */
a {
color: #667eea;
text-decoration: none;
transition: color 0.3s;
}
a:hover {
color: #764ba2;
text-decoration: underline;
}
/* 强调文本 */
strong {
color: #2c3e50;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.main-content {
margin-left: 240px;
padding: 30px 40px;
}
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main-content {
margin-left: 0;
padding: 20px;
}
.header h1 {
font-size: 28px;
}
}
</style>
</head>
</head>
<body>
<div class="container">
<!-- 侧边栏导航 -->
<aside class="sidebar">
<h2>📑 目录</h2>
<nav id="sidebar-nav">
<a href="#part0-setup" style="color: #f39c12; font-weight: 700; border-left: 3px solid #f39c12;">Part 0开发环境配置</a>
<a href="#环境初始化" class="sub-item">• 新机器初始化</a>
<a href="#日常工作流" class="sub-item">• 日常开发工作流</a>
<a href="#melos-命令速查" class="sub-item">• Melos 命令速查</a>
<a href="#part1-why">Part 1为什么Why</a>
<a href="#mono-repo-架构" class="sub-item">• Mono-Repo 架构</a>
<a href="#代码生成工具" class="sub-item">• 代码生成工具</a>
<a href="#设计理念与目标" class="sub-item">• 设计理念与目标</a>
<a href="#clean-architecture" class="sub-item-2">Clean Architecture</a>
<a href="#mvvm-状态管理" class="sub-item-2">MVVM 状态管理</a>
<a href="#为什么选择-riverpod" class="sub-item-2">为什么选择 Riverpod</a>
<a href="#feature-驱动开发" class="sub-item-2">Feature 驱动开发</a>
<a href="#模块设计哲学" class="sub-item">• 模块设计哲学</a>
<a href="#part2-structure">Part 2结构Structure</a>
<a href="#设计原则" class="sub-item">• 设计原则</a>
<a href="#solid-原则" class="sub-item-2">SOLID 原则</a>
<a href="#分层依赖规则" class="sub-item-2">分层依赖规则</a>
<a href="#模块化原则" class="sub-item-2">模块化原则</a>
<a href="#整体架构" class="sub-item">• 整体架构</a>
<a href="#2-1-整体模块图" class="sub-item-2">整体模块图</a>
<a href="#2-2-整体目录图" class="sub-item-2">整体目录图</a>
<a href="#2-3-整体分层图" class="sub-item-2">整体分层图MVVM + Riverpod</a>
<a href="#2-4-mvvm-riverpod-数据流" class="sub-item-2">MVVM + Riverpod 数据流映射</a>
<a href="#part3-concepts">Part 3核心概念</a>
<a href="#riverpod-核心概念" class="sub-item">• Riverpod 核心概念</a>
<a href="#clean-architecture-分层" class="sub-item">• Clean Architecture 分层</a>
<a href="#part4-how">Part 4怎么做How</a>
<a href="#ui-层模块详解" class="sub-item">• UI 层模块详解</a>
<a href="#3-1-ui-层职责" class="sub-item-2">UI 层职责</a>
<a href="#3-1-1-ui-层详细分层" class="sub-item-2">UI 层详细分层结构</a>
<a href="#3-2-多平台适配" class="sub-item-2">多平台适配</a>
<a href="#3-3-feature-ui-组织" class="sub-item-2">Feature UI 组织</a>
<a href="#3-4-ui-层目录结构" class="sub-item-2">UI 层目录结构</a>
<a href="#路由系统" class="sub-item">• 路由系统go_router</a>
<a href="#路由-是什么" class="sub-item-2">路由是什么</a>
<a href="#shell-是什么" class="sub-item-2">Shell 是什么</a>
<a href="#为什么禁用-navigator-push" class="sub-item-2">为什么禁止 Navigator.push</a>
<a href="#路由文件结构" class="sub-item-2">文件结构</a>
<a href="#路由路径常量" class="sub-item-2">路径常量</a>
<a href="#路由表结构" class="sub-item-2">路由表结构</a>
<a href="#shell-route-是什么" class="sub-item-2">StatefulShellRoute 是什么</a>
<a href="#如何在页面间跳转" class="sub-item-2">如何跳转</a>
<a href="#带参数路由" class="sub-item-2">带参数路由</a>
<a href="#tab-如何切换" class="sub-item-2">AppTabTab 如何切换</a>
<a href="#登录守卫" class="sub-item-2">登录守卫</a>
<a href="#refreshListenable-机制" class="sub-item-2">refreshListenable 机制</a>
<a href="#如何添加新路由" class="sub-item-2">如何添加新路由</a>
<a href="#路由守卫接入正式-token" class="sub-item-2">接入正式 token</a>
<a href="#presentation-层模块详解" class="sub-item">• Presentation 层模块详解</a>
<a href="#4-1-presentation-层职责" class="sub-item-2">Presentation 层职责</a>
<a href="#4-2-feature-presentation-组织" class="sub-item-2">Feature Presentation 组织</a>
<a href="#4-4-viewmodel-设计" class="sub-item-2">ViewModel 设计</a>
<a href="#domain-层模块详解" class="sub-item">• Domain 层模块详解</a>
<a href="#5-1-domain-层职责" class="sub-item-2">Domain 层职责</a>
<a href="#5-5-use-case-设计" class="sub-item-2">Use Case 设计</a>
<a href="#5-7-repository-接口" class="sub-item-2">Repository 接口</a>
<a href="#data-层模块详解" class="sub-item">• Data 层模块详解</a>
<a href="#6-1-data-层职责" class="sub-item-2">Data 层职责</a>
<a href="#6-3-repository-实现" class="sub-item-2">Repository 实现</a>
<a href="#6-4-data-source-详解" class="sub-item-2">Data Source 详解</a>
<a href="#core-层模块详解" class="sub-item">• Core 层模块详解</a>
<a href="#7-1-core-层职责" class="sub-item-2">Core 层职责</a>
<a href="#7-2-核心-sdk" class="sub-item-2">基础设施</a>
<a href="#7-3-核心-sdk" class="sub-item-2">核心 SDK</a>
<a href="#7-4-l10n" class="sub-item-2">多语言国际化</a>
<a href="#7-5-core-ui" class="sub-item-2">Core UI</a>
<a href="#扩展性设计" class="sub-item">• 扩展性设计</a>
<a href="#项目配置" class="sub-item">• 项目配置</a>
<a href="#part5-examples">Part 5数据流转示例</a>
<a href="#9-1-发送消息流程" class="sub-item">• 发送消息流程</a>
<a href="#9-3-加载会话列表流程" class="sub-item">• 加载会话列表流程</a>
<a href="#9-4-跨-feature-交互" class="sub-item">• 跨 Feature 交互</a>
<a href="#part6-enterprise">Part 6企业级架构</a>
<a href="#5-1-bridge-能力规划" class="sub-item">• Bridge 能力规划</a>
<a href="#5-2-数据获取策略" class="sub-item">• 数据获取多层策略</a>
<a href="#5-3-中间层设计" class="sub-item">• 中间层设计</a>
<a href="#5-4-系统能力划分" class="sub-item">• 系统能力划分</a>
<a href="#5-5-代码-review-机制" class="sub-item">• Code Review 机制</a>
<a href="#5-6-长期收益" class="sub-item">• 长期收益分析</a>
<a href="#5-7-日志与监控系统" class="sub-item">• 日志与监控系统</a>
<a href="#part7-summary">Part 7总结</a>
</nav>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<div class="header">
<h1>IM App 整体架构设计</h1>
<blockquote>
<p>小型 IM 应用完整架构方案</p>
<p>Clean Architecture / MVVM / Feature 驱动 / 高内聚低耦合 / 严格分层</p>
</blockquote>
</div>
<h2 id="part0-setup" style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: white; padding: 20px; border-radius: 8px; margin-top: 0;">Part 0开发环境配置</h2>
<p>阅读架构之前先把环境跑起来,大约 5 分钟。</p>
<h3 id="环境初始化">新机器初始化(只需做一次)</h3>
<h4>第零步:从 Gitea 拉取代码</h4>
<p>项目托管在内部 Gitea仅支持 HTTPS 访问SSH 暂不开放)。需要先在 Gitea 生成个人访问令牌Personal Access Token</p>
<p><strong>生成 Token 步骤:</strong></p>
<ol>
<li>登录 Gitea<code>https://gitea.winwayinfo.com</code></li>
<li>点击右上角头像 → <strong>Settings</strong></li>
<li>左侧菜单选 <strong>Applications</strong></li>
<li>在 "Generate New Token" 填写令牌名称(任意)</li>
<li><strong>Repository and organization Access</strong> 下选择 <strong>All</strong></li>
<li><strong>Selected token permissions</strong> 下所有权限选 <strong>Read and Write</strong></li>
<li>点击 <strong>Generate Token</strong>,复制生成的 token只显示一次</li>
</ol>
<p><strong>克隆仓库:</strong></p>
<pre><code># 将 YOUR_TOKEN 替换为刚才生成的 tokenYOUR_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
# 输入用户名和 tokentoken 作为密码)
</code></pre>
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<strong>注意:</strong>
<ul style="margin-bottom: 0;">
<li>Token 只在生成时显示一次,务必立即保存</li>
<li>如果 clone 时提示输入密码,输入的是 Token 而不是账号密码</li>
<li>为避免每次 push/pull 都输入,可在克隆地址里内嵌 token<code>https://user:token@gitea.winwayinfo.com/...</code></li>
<li>切换到 <code>dev</code> 分支开发:<code>git checkout dev</code><code>main</code> 为主干保护分支</li>
</ul>
</div>
<h4>第一步:安装 Flutter SDK</h4>
<ol>
<li>前往 <a href="https://docs.flutter.dev/get-started/install/macos/mobile-ios" target="_blank">flutter.dev</a> 下载最新 stable channel macOS 版本(.tar.xz
<div style="background: #ffebee; padding: 10px; border-radius: 6px; border-left: 4px solid #c62828; margin: 8px 0;">
<strong>Apple SiliconM 系列)必须下载 arm64 版本。</strong><br>
文件名含 <code>arm64</code> 的才是 Apple Silicon 版,例如 <code>flutter_macos_arm64_3.x.x-stable.tar.xz</code><br>
不含 <code>arm64</code> 的(如 <code>flutter_macos_3.x.x-stable.tar.xz</code>)是 Intel (x86_64) 版,装在 M 芯片上编译 macOS 时会报:<br>
<code>Unable to find a device matching { platform:macOS, arch:arm64 }</code>
</div>
</li>
<li>解压到固定目录,推荐 <code>~/flutter</code>
<pre><code>cd ~
tar xf ~/Downloads/flutter_macos_arm64_*.tar.xz
# 解压后目录为 ~/flutter
</code></pre>
</li>
<li>写入环境变量:
<pre><code>echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.zshrc
</code></pre>
</li>
<li>验证(在当前终端临时生效):
<pre><code>source ~/.zshrc
flutter --version
dart --version
# 确认 Dart 二进制是 arm64Apple Silicon 机器)
file $(which dart) # 应输出 Mach-O 64-bit executable arm64
</code></pre>
</li>
</ol>
<h4>第二步:安装 Homebrew + Ruby + CocoaPodsiOS / macOS 必须)</h4>
<p>iOS 和 macOS 平台依赖 CocoaPods 管理原生依赖CocoaPods 需要 Ruby 3.x通过 Homebrew 安装。</p>
<div style="background: #ffebee; padding: 10px; border-radius: 6px; border-left: 4px solid #c62828; margin: 10px 0;">
<strong>必须安装 Ruby 3.3不能装最新版4.x</strong><br>
Ruby 4.0 + OpenSSL 3.6 在 macOS 26 beta 上与 <code>cdn.cocoapods.org</code> 的 TLS 握手不兼容,<code>pod install</code> 会报
<code>Connection reset by peer - SSL_connect</code>,导致 iOS / macOS 构建失败。
</div>
<pre><code># 1. 安装 Homebrew需要输入开机密码
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. 将 Homebrew 本身写入 PATHApple 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
</code></pre>
<p>第 10 步单独说明——在 <code>~/.zshrc</code> 末尾手动追加以下内容(这段是写入文件的 shell 代码,不是在终端直接执行的命令):</p>
<pre><code># CocoaPods CDN SSL fix for macOS 26 beta
# 修复原因macOS 26 beta + OpenSSL 3.x 与 cdn.cocoapods.org TLS 握手失败
# 每次开终端自动写入临时配置文件并设置 OPENSSL_CONFpod install 时读取该配置
cat &gt; /tmp/openssl_pod.cnf &lt;&lt; '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
</code></pre>
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<strong>注意:</strong>
<ul style="margin-bottom: 0;">
<li>必须用 <code>brew install ruby@3.3</code>,不能用 <code>brew install ruby</code>(后者会装最新的 4.x</li>
<li>Homebrew 装好后必须先把 <code>/opt/homebrew/bin</code> 写入 PATH否则终端找不到 <code>brew</code> 命令</li>
<li>macOS 自带 Ruby 2.6CocoaPods 要求 &gt;= 3.0,必须通过 Homebrew 安装新版</li>
<li>OPENSSL_CONF 修复是 <em>写在 ~/.zshrc 里的 shell 代码段</em>,每次开新终端自动执行,不是一次性命令</li>
<li>iOS / macOS 编译还需要完整安装 <strong>Xcode</strong>,仅有 Command Line Tools 不够(见下一步)</li>
</ul>
</div>
<h4>(补充)安装 XcodeiOS / macOS 编译必须)</h4>
<p>CocoaPods 只是依赖管理工具,实际编译 iOS / macOS 需要完整的 Xcode。Command Line Tools 不够。</p>
<ol>
<li>在 App Store 搜索 <strong>Xcode</strong> 并安装(约 10 GB</li>
<li>安装完成后执行:
<pre><code>sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
xcodebuild -version # 验证
</code></pre>
</li>
</ol>
<div style="background: #ffebee; padding: 12px; border-radius: 8px; border-left: 4px solid #c62828; margin: 15px 0;">
<strong>不装 Xcode 的后果:</strong><code>flutter build ios</code> 报 "Application not configured for iOS"<code>flutter build macos</code> 报 "unable to find utility xcodebuild",两个平台都无法编译。
</div>
<h4>第三步:配置 IDE</h4>
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; border-left: 4px solid #0288d1; margin: 15px 0;">
<p style="margin-top: 0; font-weight: 700; color: #0288d1;">Android Studio</p>
<ol style="margin-bottom: 0;">
<li>打开 Android Studio</li>
<li>菜单:<code>SettingsWindows/Linux</code><code>PreferencesmacOS</code></li>
<li>导航至 <code>Languages &amp; Frameworks → Flutter</code></li>
<li>Flutter SDK path 填写解压路径,例如 <code>/Users/{你的用户名}/flutter</code></li>
<li>点击 Apply → OK</li>
<li>重启 Android Studio</li>
</ol>
</div>
<div style="background: #f3e5f5; padding: 15px; border-radius: 8px; border-left: 4px solid #7b1fa2; margin: 15px 0;">
<p style="margin-top: 0; font-weight: 700; color: #7b1fa2;">VS Code</p>
<ol style="margin-bottom: 0;">
<li>在 Extensions 市场搜索并安装 <strong>Flutter</strong> 插件Dart 插件会自动一并安装)</li>
<li><code>Cmd+Shift+P</code> 打开命令面板</li>
<li>输入 <code>Flutter: Change SDK</code>,选择 Flutter SDK 解压目录(例如 <code>/Users/{你的用户名}/flutter</code></li>
<li>重启 VS Code</li>
</ol>
</div>
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<strong>注意:</strong>配置完成后,<strong>彻底退出所有终端和 IDE包括 IDE 内嵌终端)</strong>,再继续后续步骤。否则新写入的 PATH 不会在已开启的终端中生效。
</div>
<h4>第三步:运行初始化脚本</h4>
<p>重新打开终端,进入项目根目录执行:</p>
<pre><code>cd customer-im-client
bash scripts/setup.sh
</code></pre>
<p>setup.sh 包含 6 步:安装全局工具 → 配置 PATH → dart pub get → melos bootstrap → mason get → melos run gen。
其中 <code>melos bootstrap</code> 负责生成 <code>.iml</code><code>.idea/modules.xml</code>IDE 模块识别,不入库)。</p>
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<strong>setup.sh 执行完毕后,再次彻底退出所有终端和 IDE。</strong>然后重新打开 IDE。
<br>若提示 <code>melos: command not found</code>,说明 PATH 未生效,执行 <code>source ~/.zshrc</code> 后再重试。
<br>若 IDE 缺少 <code>[melos_xxx_sdk]</code> 标签,单独执行 <code>melos bootstrap</code> 即可补全。
</div>
<h4>第四步:验证项目可以运行</h4>
<p>重新打开 IDE在内嵌终端或新终端窗口中<strong>切换到 im_app 目录</strong>执行:</p>
<pre><code>cd customer-im-client/apps/im_app
flutter run
</code></pre>
<p>首次运行会触发 Gradle / CocoaPods 等平台工具链下载,耐心等待即可。</p>
<h4>第五步:各平台单独跑一次 pub get</h4>
<p>为避免平台插件注册缺失导致运行失败,每个目标平台在首次开发前单独执行一次:</p>
<pre><code># 在 apps/im_app 目录下执行
flutter pub get # 所有平台依赖
# 按需对各平台单独验证
flutter build apk --debug # Android
flutter build ios --debug --no-codesign # iOSmacOS 机器)
flutter build macos --debug # macOS
</code></pre>
<p><strong>完成以上步骤后,<code>~/.zshrc</code> 末尾应包含以下内容(供核对):</strong></p>
<pre><code>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
</code></pre>
<div style="background: #e8f5e9; padding: 12px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 15px 0;">
<strong>完成以上步骤后环境即可正常使用。</strong>后续只需保持 <code>melos run gen:watch</code> 常驻运行即可(见下方日常工作流)。
</div>
<h3 id="日常工作流">日常开发工作流</h3>
<p>每次开发时开两个终端窗口:</p>
<table>
<thead><tr><th>终端</th><th>命令</th><th>说明</th></tr></thead>
<tbody>
<tr><td>Terminal 1常驻</td><td><code>melos run gen:watch</code></td><td>⚠️ 必须保持运行,保存代码后自动生成 .g.dart</td></tr>
<tr><td>Terminal 2</td><td><code>flutter run</code></td><td>启动 App支持热重载</td></tr>
</tbody>
</table>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<p style="margin-top: 0; font-weight: 700; color: #f57f17;">代码生成常见问题</p>
<ul style="margin-bottom: 0;">
<li><strong>保存后红线不消失</strong>:检查 Terminal 1 是否有 <code>melos run gen:watch</code> 在运行</li>
<li><strong>生成报错</strong><code>melos run gen</code> 重新全量生成</li>
<li><strong>新增依赖后</strong>:先 <code>dart pub get</code>,再重启 watch</li>
<li><strong>.g.dart 冲突</strong>:直接删除冲突文件后重新生成,不要手动合并</li>
</ul>
</div>
<h3 id="melos-命令速查">Melos 命令速查</h3>
<table>
<thead><tr><th>命令</th><th>说明</th></tr></thead>
<tbody>
<tr><td colspan="2" style="background:#f5f5f5; font-weight:600; color:#555;">依赖 &amp; 代码生成</td></tr>
<tr><td><code>bash scripts/setup.sh</code></td><td>新机器一键环境初始化(只需执行一次)</td></tr>
<tr><td><code>melos bootstrap</code></td><td>生成 IDE 模块配置(.iml + modules.xml缺 [melos_xxx] 标签时执行</td></tr>
<tr><td><code>dart pub get</code></td><td>安装所有依赖(首次或 pubspec 变更后)</td></tr>
<tr><td><code>melos run gen</code></td><td>一次性代码生成(.g.dart / .freezed.dart</td></tr>
<tr><td><code>melos run gen:watch</code></td><td>⚠️ 开发必开:监听模式,保存后自动生成</td></tr>
<tr><td colspan="2" style="background:#f5f5f5; font-weight:600; color:#555;">质量检查</td></tr>
<tr><td><code>melos run analyze</code></td><td>所有 Package 静态分析</td></tr>
<tr><td><code>melos run test</code></td><td>所有 Package 单元测试</td></tr>
<tr><td colspan="2" style="background:#f5f5f5; font-weight:600; color:#555;">清理</td></tr>
<tr><td><code>melos run clean</code></td><td>所有 Package flutter clean</td></tr>
<tr><td><code>melos run clean:deep</code></td><td>深度清理(含 Gradle / Pods / CMake + 生成文件)</td></tr>
<tr><td><code>melos run clean:deep -- android</code></td><td>只清 Android 平台缓存ios / macos / windows 同理)</td></tr>
<tr><td colspan="2" style="background:#f5f5f5; font-weight:600; color:#555;">SDK 版本管理</td></tr>
<tr><td><code>melos run sdk:bump</code></td><td>从 flutter.dev 拉最新稳定版Dart + Flutter 一起升级</td></tr>
<tr><td><code>melos run sdk:bump -- --dart 3.12.0</code></td><td>只升 Dart SDK 约束Flutter 下限不变</td></tr>
<tr><td><code>melos run sdk:bump -- --flutter 3.40.0</code></td><td>只升 Flutter 下限Dart 不变</td></tr>
<tr><td><code>melos run sdk:bump -- --dart 3.12.0 --flutter 3.40.0</code></td><td>手动指定两者(用于 CI 固定版本)</td></tr>
<tr><td colspan="2" style="background:#f5f5f5; font-weight:600; color:#555;">构建</td></tr>
<tr><td><code>melos run new:sdk -- push</code></td><td>创建新 SDK 包 packages/push_sdk/Facade+Wiring 骨架)</td></tr>
<tr><td><code>melos run remove:sdk -- push</code></td><td>删除 SDK 包,同步清理 workspace、IDE 模块注册</td></tr>
<tr><td><code>melos run build:android:apk</code></td><td>构建 Android APK</td></tr>
<tr><td><code>melos run build:android:aab</code></td><td>构建 Android AABGoogle Play</td></tr>
<tr><td><code>melos run build:ios</code></td><td>构建 iOS IPA仅 macOS</td></tr>
<tr><td><code>melos run build:macos</code></td><td>构建 macOS app</td></tr>
<tr><td><code>melos run build:windows</code></td><td>构建 Windows EXE仅 Windows</td></tr>
</tbody>
</table>
<hr>
<h2 id="part1-why" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-top: 0;">第一部分为什么Why- 设计理念</h2>
<h2 id="mono-repo-架构">Mono-Repo 架构</h2>
<p><strong>项目组织方式</strong>:本项目采用 <strong>Mono-Repo</strong>(单一代码仓库)方式组织,使用 <strong>Melos</strong> 进行管理。</p>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #0288d1; margin: 20px 0;">
<h4 style="margin-top: 0; color: #0288d1;">什么是 Mono-Repo</h4>
<p>Mono-Repo 是将多个相关项目放在同一个代码仓库中管理的方式与传统的每个项目一个仓库Multi-Repo不同。</p>
<p><strong>我们的 Mono-Repo 包含:</strong></p>
<ul style="margin-bottom: 0;">
<li>主应用IM App</li>
<li>9 个 Core SDKNetworkSDK、StorageSDK、ProtocolSDK、MediaSDK、RTCSDK、NotificationSDK、CipherGuardSDK、CryptoSDK、L10nSDK</li>
<li>共享组件库Widgets、Utils、Extensions</li>
<li>示例应用和测试项目</li>
</ul>
</div>
<h4>为什么选择 Mono-Repo</h4>
<table>
<thead>
<tr>
<th>优势</th>
<th>说明</th>
<th>实际价值</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>版本一致性</strong></td>
<td>同一个 commit 保证所有 package 兼容</td>
<td>不会出现版本不匹配问题</td>
</tr>
<tr>
<td><strong>API 变更安全</strong></td>
<td>编译期立即发现问题,马上修复</td>
<td>不会在发版后才发现问题</td>
</tr>
<tr>
<td><strong>重构成本低</strong></td>
<td>一次性全 repo 重构</td>
<td>不需要跨 repo、分批跟进</td>
</tr>
<tr>
<td><strong>开发效率高</strong></td>
<td>改 SDK → 主应用立即验证</td>
<td>不需要先发版才能验证</td>
</tr>
<tr>
<td><strong>统一工具链</strong></td>
<td>一套 Melos 指令管理所有项目</td>
<td>不需要维护多套脚本</td>
</tr>
<tr>
<td><strong>新人友好</strong></td>
<td>clone 一个 repo 就全到位</td>
<td>不需要 clone 多个 repo</td>
</tr>
</tbody>
</table>
<h4>Melos 管理工具</h4>
<p><strong>Melos</strong> 是 Flutter/Dart 生态的 Mono-Repo 管理工具,提供:</p>
<ul>
<li><strong>自动依赖解析</strong>:自动 link 本地 package无需手动管理</li>
<li><strong>统一脚本命令</strong>:一条命令运行所有测试、构建、发布</li>
<li><strong>增量测试</strong>:只测试受影响的 packages节省时间</li>
<li><strong>版本管理</strong>:统一管理所有 package 的版本号</li>
</ul>
<div class="mermaid">
flowchart TD
App[主应用<br/>IM App]
App --> Root[IM Mono-Repo<br/>根目录]
Root --> SDKs[Core SDKs<br/>9个独立SDK]
Root --> Shared[共享组件<br/>Widgets/Utils]
Root --> Examples[示例应用<br/>Example Apps]
SDKs --> SDK1[NetworkSDK]
SDKs --> SDK2[StorageSDK]
SDKs --> SDK3[ProtocolSDK]
SDKs --> SDK4[MediaSDK]
SDKs --> SDK5[RTCSDK]
SDKs --> SDK6[NotificationSDK]
SDKs --> SDK7[CipherGuardSDK<br/>Flutter Plugin]
SDKs --> SDK8[CryptoSDK<br/>占位]
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
</div>
<p style="color: #666; font-style: italic; margin-top: 20px;">
<strong>设计理念</strong>:通过 Mono-Repo + Melos 的组合,我们能够在保持模块独立性的同时,获得统一管理的便利性,大幅提升开发效率和代码质量。
</p>
<h3>Packages 目录结构SDK 独立 Package</h3>
<p>所有可复用的基础能力 SDK 从主 App 的 <code>core/foundation/</code> 提取为独立 Package放在 Mono-Repo 的 <code>packages/</code> 目录下。主 App 通过 <code>pubspec.yaml</code><code>path:</code> 依赖引用。</p>
<pre><code>packages/
├── networks_sdk/ # 网络通信HTTP + WebSocketFlutter Plugin
│ ├── build.yaml # @ApiRequest 代码生成器注册
│ └── lib/
│ ├── networks_sdk.dart # barrel file统一导出
│ └── src/
│ ├── annotations/
│ │ └── api_request.dart # @ApiRequest 注解定义
│ ├── generator/
│ │ ├── api_request_generator.dart # build_runner 代码生成器实现
│ │ └── builder.dart # SharedPartBuilder 入口
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── http/
│ │ │ │ ├── api_client.dart # Dio REST 客户端
│ │ │ │ └── interceptor/
│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
│ │ │ └── socket/
│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream
│ │ ├── dto/
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
│ │ └── repositories/
│ │ ├── networks_sdk_repository_impl.dart
│ │ └── networks_messaging_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── api_error.dart # @freezed HTTP 错误联合类型
│ │ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
│ │ │ ├── socket_connection_state.dart # 连接状态 enum
│ │ │ ├── http_method.dart # GET/POST/PUT/DELETE/PATCH
│ │ │ └── api_request_type.dart # request/login/upload
│ │ └── repositories/
│ │ ├── networks_sdk_repository.dart
│ │ └── networks_messaging_repository.dart
│ └── presentation/
│ ├── facade/
│ │ ├── networks_sdk_api.dart # HTTP 公开 API 接口
│ │ └── networks_messaging_api.dart # WebSocket 公开 API 接口
│ └── wiring/
│ ├── api_config.dart # HTTP 配置baseURL/Token/回调)
│ ├── socket_config.dart # WebSocket 配置(心跳/重连策略)
│ ├── network_callbacks.dart # 回调类型定义
│ ├── networks_sdk_core.dart
│ ├── networks_sdk_api_impl.dart
│ ├── networks_messaging_api_impl.dart
│ └── networks_sdk_wiring.dart # 工厂build() / buildMessagingApi()
├── cipher_guard_sdk/ # 端对端加密Flutter Plugin
│ └── lib/
│ ├── cipher_guard_sdk.dart # barrel file
│ └── src/
│ ├── data/
│ │ ├── constants/
│ │ │ └── method_channel_constants.dart
│ │ ├── datasources/
│ │ │ ├── encryption_flutter_service.dart # RSA/AES 纯 Dart 加解密pointycastle + encrypt
│ │ │ └── encryption_method_channel.dart # Native 密钥同步iOS App Group
│ │ ├── dto/
│ │ │ ├── chat_encryption_key_dto.dart
│ │ │ ├── encrypted_message_dto.dart
│ │ │ ├── method_channel_response.dart
│ │ │ └── rsa_key_pair_dto.dart
│ │ └── repositories/
│ │ └── encryption_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── chat_encryption_key.dart
│ │ │ ├── encrypted_message.dart
│ │ │ ├── rsa_key_pair.dart
│ │ │ └── session_key.dart
│ │ └── repositories/
│ │ └── encryption_repository.dart
│ └── presentation/
│ ├── facade/
│ │ └── cipher_guard_sdk_api.dart # 公开 API调用方只依赖这里
│ └── wiring/
│ ├── cipher_guard_sdk_api_impl.dart
│ ├── cipher_guard_sdk_core.dart
│ └── cipher_guard_sdk_wiring.dart
├── storage_sdk/ # 本地存储Facade+Wiring纯基础设施连接管理 + 通用 CRUD
│ └── lib/
│ ├── storage_sdk.dart # barrel导出 StorageSdkApi
│ └── src/
│ ├── data/
│ │ ├── local/
│ │ │ └── datasources/
│ │ │ └── database_datasource.dart # 连接生命周期openDatabase/closeDatabase
│ │ └── repositories/
│ │ └── database_repository_impl.dart # 通用 CRUD 的 Drift 实现
│ ├── domain/
│ │ └── repositories/
│ │ └── database_repository.dart # 通用 CRUD 接口(泛型,与表无关)
│ └── presentation/
│ ├── facade/
│ │ └── storage_sdk_api.dart # 公开接口(生命周期 + 全量 CRUD
│ └── wiring/
│ ├── storage_sdk_api_impl.dart # 委托给 Core
│ ├── storage_sdk_core.dart # 持有 DataSource + Repo
│ └── storage_sdk_wiring.dart # build(databaseFactory:) → StorageSdkApi
│ # ── 以下 SDK 均遵循相同的 Facade + Wiring 模式,结构参照 cipher_guard_sdk ──
├── notification_sdk/ # 推送通知Flutter Plugin
│ └── lib/notification_sdk.dart + src/{data,domain,presentation}/ (同上模式)
├── media_sdk/ # 媒体处理Flutter Plugin
│ └── lib/media_sdk.dart + src/{data,domain,presentation}/ (同上模式)
├── rtc_sdk/ # 实时音视频Flutter Plugin
│ └── lib/rtc_sdk.dart + src/{data,domain,presentation}/ (同上模式)
├── protocol_sdk/ # 消息协议Flutter Plugin
│ └── lib/protocol_sdk.dart + src/{data,domain,presentation}/ (同上模式)
├── l10n_sdk/ # 国际化Flutter Plugin
│ └── lib/l10n_sdk.dart + src/{data,domain,presentation}/ (同上模式)
</code></pre>
<p><strong>Package 类型说明:</strong></p>
<ul>
<li><strong>Flutter Plugin</strong>(多个):所有 SDK 均声明为 Flutter Plugin包含 <code>android/</code> + <code>ios/</code> 原生代码入口Plugin 机制自动注册</li>
<li><strong>cipher_guard_sdk</strong>E2E 加密核心RSA/AES 双层加密 + iOS App Group 密钥同步(用于推送通知解密)</li>
</ul>
<p><strong>主 App 引用方式pubspec.yaml</strong></p>
<pre><code>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
</code></pre>
<p><strong>设计原则:</strong></p>
<ul>
<li><strong>独立发布</strong>:每个 SDK 可独立版本号、独立 CHANGELOG未来可发布到 pub.dev</li>
<li><strong>跨项目复用</strong>:其他 Flutter 项目可直接依赖这些 SDK不绑定 IM App 业务逻辑</li>
<li><strong>最小依赖</strong>SDK 之间尽量无依赖,必要时通过接口解耦</li>
<li><strong>Melos 统一管理</strong><code>dart pub get</code> 统一安装依赖,<code>melos run test</code> 批量测试</li>
</ul>
<hr>
<hr>
<h2 id="代码生成工具">代码生成工具</h2>
<p>本项目大量使用代码生成工具来提升开发效率、减少样板代码,并保证类型安全。</p>
<h3>核心代码生成工具</h3>
<table>
<thead>
<tr>
<th>工具</th>
<th>作用</th>
<th>生成内容</th>
<th>优势</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>riverpod_generator</strong></td>
<td>Riverpod Provider 代码生成</td>
<td>自动生成 Provider、依赖注入代码</td>
<td>大幅减少样板代码,编译期类型安全</td>
</tr>
<tr>
<td><strong>freezed</strong></td>
<td>不可变数据类生成</td>
<td>State 类、copyWith、序列化代码</td>
<td>消除手写 State 的繁琐,保证不可变性</td>
</tr>
<tr>
<td><strong>json_serializable</strong></td>
<td>JSON 序列化/反序列化</td>
<td>fromJson/toJson 方法</td>
<td>自动处理 JSON 转换,类型安全</td>
</tr>
<tr>
<td><strong>build_runner</strong></td>
<td>代码生成执行器</td>
<td>监听文件变化,自动生成</td>
<td>开发时实时生成,无需手动执行</td>
</tr>
</tbody>
</table>
<h3>代码生成示例对比</h3>
<h4>不使用代码生成(手写样板代码)</h4>
<pre><code class="language-dart">// 手写 State 类 - 繁琐且容易出错
class ChatState {
final List&lt;Message&gt; messages;
final bool isLoading;
final String error;
const ChatState({
required this.messages,
required this.isLoading,
required this.error,
});
// 手写 copyWith - 每个字段都要写
ChatState copyWith({
List&lt;Message&gt;? 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 &amp;&amp;
listEquals(other.messages, messages) &amp;&amp;
other.isLoading == isLoading &amp;&amp;
other.error == error;
}
@override
int get hashCode =&gt; Object.hash(messages, isLoading, error);
}
// 手写 Provider - 样板代码多
final chatViewModelProvider = StateNotifierProvider.autoDispose&lt;
ChatViewModel, ChatState
&gt;((ref) {
final sendMessageUseCase = ref.watch(sendMessageUseCaseProvider);
final loadMessagesUseCase = ref.watch(loadMessagesUseCaseProvider);
return ChatViewModel(sendMessageUseCase, loadMessagesUseCase);
});
</code></pre>
<h4>使用代码生成(简洁高效)</h4>
<pre><code class="language-dart">// 使用 @freezed - 自动生成 copyWith、equality、toString
part 'chat_state.freezed.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List&lt;Message&gt; 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() =&gt; const ChatState();
Future&lt;void&gt; sendMessage(String content) async {
state = state.copyWith(isLoading: true);
await ref.read(sendMessageUseCaseProvider)(content);
state = state.copyWith(isLoading: false);
}
}
</code></pre>
<h3>开发流程</h3>
<div class="mermaid">
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
</div>
<h3>代码生成命令</h3>
<pre><code class="language-bash"># 一次性生成
melos run gen
# 监听模式(开发期间必须常驻)
melos run gen:watch
</code></pre>
<h3>代码生成的价值</h3>
<ul>
<li><strong>大幅减少样板代码</strong>:不需要手写 copyWith、equality、hashCode</li>
<li><strong>编译期类型安全</strong>:生成的代码类型完全正确,无运行时错误</li>
<li><strong>自动化维护</strong>:修改字段后自动重新生成,无需手动同步</li>
<li><strong>统一代码风格</strong>:生成的代码风格一致,易于 Code Review</li>
<li><strong>提升开发效率</strong>:专注业务逻辑,不浪费时间在重复代码上</li>
</ul>
<hr>
<hr>
<h2 id="设计理念与目标">设计理念与目标</h2>
<h3 id="clean-architecture">Clean Architecture整洁架构</h3>
<p><strong>目的</strong>:让业务逻辑与界面设计分离</p>
<p><strong>方法</strong>:通过结构的分层来约束类别间的使用方向</p>
<p><strong>好处</strong></p>
<ul>
<li>代码更易维护</li>
<li>代码更易测试</li>
<li>代码更易扩展</li>
<li>业务逻辑独立于框架</li>
<li>业务逻辑独立于UI</li>
<li>业务逻辑独立于数据库</li>
</ul>
<h3 id="mvvm-状态管理">MVVMModel-View-ViewModel状态管理</h3>
<h4>演进历史</h4>
<p>在响应式应用中,状态管理经历了以下演进过程:</p>
<div class="mermaid">
flowchart LR
MVC[MVC<br/>Model-View-Controller] --> MVP[MVP<br/>Model-View-Presentation]
MVP --> MVVM[MVVM<br/>Model-View-ViewModel]
style MVC fill:#fce4ec,stroke:#c2185b
style MVP fill:#fff4e6,stroke:#f57c00
style MVVM fill:#e8f5e9,stroke:#388e3c
</div>
<h4>为什么要这样演变?</h4>
<table>
<thead>
<tr>
<th>架构模式</th>
<th>核心问题</th>
<th>演进动机</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>MVC</strong><br/>(Model-View-Controller)</td>
<td>
<ul style="margin: 0; padding-left: 20px;">
<li>Controller 职责过重,成为"上帝类"</li>
<li>View 和 Model 耦合,难以测试</li>
<li>View 直接访问 Model导致业务逻辑泄露到 UI</li>
<li>难以进行单元测试(需要 UI 环境)</li>
</ul>
</td>
<td>需要更好的关注点分离和可测试性</td>
</tr>
<tr>
<td><strong>MVP</strong><br/>(Model-View-Presenter)</td>
<td>
<ul style="margin: 0; padding-left: 20px;">
<li>View 和 Presenter 通过接口通信,代码量大</li>
<li>Presenter 需要手动更新 View调用 view.updateXXX()</li>
<li>数据变化时需要手动同步到 UI</li>
<li>样板代码多,维护成本高</li>
</ul>
</td>
<td>需要自动化的数据绑定和响应式更新</td>
</tr>
<tr>
<td><strong>MVVM</strong><br/>(Model-View-ViewModel)</td>
<td>
<ul style="margin: 0; padding-left: 20px;">
<li>通过数据绑定实现 UI 自动更新</li>
<li>ViewModel 不持有 View 引用,完全解耦</li>
<li>状态变化自动反映到 UI无需手动刷新</li>
<li>更易测试ViewModel 可独立测试</li>
</ul>
</td>
<td>响应式编程的最佳实践</td>
</tr>
</tbody>
</table>
<h5>演进的本质:逐步解耦和自动化</h5>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #0288d1; margin: 20px 0;">
<ol style="margin: 0;">
<li><strong>MVC → MVP</strong>:解决 View 和 Model 的耦合
<ul>
<li>引入 Presenter 作为中介View 不再直接访问 Model</li>
<li>View 和 Presenter 通过接口通信,提升可测试性</li>
<li><strong>缺点</strong>:手动更新 UI 的样板代码过多</li>
</ul>
</li>
<li><strong>MVP → MVVM</strong>:引入数据绑定,实现自动化
<ul>
<li>ViewModel 暴露可观察的状态State</li>
<li>View 通过数据绑定自动订阅状态变化</li>
<li>状态更新时UI 自动刷新,无需手动调用</li>
<li><strong>优势</strong>:代码更简洁,逻辑更清晰,易于维护</li>
</ul>
</li>
</ol>
</div>
<h4>前提条件</h4>
<p>状态管理方式高度依赖官方 SDK 的支持与否才可以实现。如果官方 SDK 不支持,某些框架将无法实现。</p>
<p><strong>实例</strong></p>
<ul>
<li>2012-2019年Android 开发只支持 MVC 状态管理,无法使用 MVVM</li>
<li>2020年Android 官方推出了 BindingView 的 SDK此后才可以使用 MVVM 做开发</li>
<li>Flutter从一开始就支持响应式框架天然适合 MVVM</li>
</ul>
<h4>技术栈规定</h4>
<div style="background: #fff3cd; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p style="margin-top: 0;"><strong>技术栈升级要求</strong></p>
<p>为保证架构的现代化和统一性,必须采用以下技术栈:</p>
</div>
<table>
<thead>
<tr>
<th>平台</th>
<th>UI 框架</th>
<th>状态管理</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>iOS</strong></td>
<td>SwiftUI</td>
<td>Combine + Observation</td>
<td>Apple 官方声明式 UI + 响应式框架</td>
</tr>
<tr>
<td><strong>Android</strong></td>
<td>Jetpack Compose</td>
<td>Flow + LiveData</td>
<td>Google 官方声明式 UI + 响应式框架</td>
</tr>
<tr>
<td><strong>Flutter</strong></td>
<td>Widget</td>
<td>Riverpod</td>
<td>跨平台声明式 UI + 现代状态管理</td>
</tr>
</tbody>
</table>
<h5>为什么强制使用这些技术栈?</h5>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<ul style="margin: 0;">
<li><strong>统一的架构思想</strong>:三端都采用声明式 UI + 响应式状态管理,降低学习成本</li>
<li><strong>官方推荐方案</strong>SwiftUI、Compose 分别是 Apple 和 Google 的官方推荐技术栈</li>
<li><strong>现代化开发</strong>:摒弃 UIKit/XML 等过时技术,拥抱声明式编程范式</li>
<li><strong>更好的性能</strong>:声明式 UI 框架在渲染性能和内存管理上都更优秀</li>
<li><strong>易于维护</strong>代码更简洁、逻辑更清晰、bug 更少</li>
<li><strong>团队协作</strong>:统一技术栈降低沟通成本,提高开发效率</li>
</ul>
</div>
<h5>学习资源</h5>
<p><strong>iOS - SwiftUI + Combine + Observation</strong></p>
<ul>
<li><a href="https://developer.apple.com/tutorials/swiftui" target="_blank">SwiftUI 官方教程</a> - Apple 官方 SwiftUI 完整学习路径</li>
<li><a href="https://developer.apple.com/documentation/swiftui" target="_blank">SwiftUI 官方文档</a> - SwiftUI 完整 API 文档</li>
<li><a href="https://developer.apple.com/documentation/combine" target="_blank">Combine 官方文档</a> - Apple 响应式框架完整文档</li>
<li><a href="https://developer.apple.com/documentation/observation" target="_blank">Observation 官方文档</a> - Swift 现代化可观察对象框架iOS 17+</li>
</ul>
<p><strong>Android - Jetpack Compose + Flow + LiveData</strong></p>
<ul>
<li><a href="https://developer.android.com/courses/jetpack-compose/course" target="_blank">Jetpack Compose 官方课程</a> - Google 官方 Compose 学习路径</li>
<li><a href="https://developer.android.com/jetpack/compose" target="_blank">Jetpack Compose 官方文档</a> - Compose 完整文档</li>
<li><a href="https://developer.android.com/kotlin/flow" target="_blank">Kotlin Flow 官方文档</a> - Kotlin 协程和 Flow 完整指南</li>
<li><a href="https://developer.android.com/topic/libraries/architecture/livedata" target="_blank">LiveData 官方文档</a> - LiveData 使用指南</li>
</ul>
<h4>参考学习链接</h4>
<p>深入了解 Flutter 应用架构和 MVVM 模式:</p>
<ul>
<li><a href="https://docs.flutter.dev/app-architecture/guide" target="_blank">Flutter 官方架构指南</a> - Flutter 官方推荐的应用架构设计指南</li>
<li><a href="https://riverpod.dev/docs/introduction/getting_started" target="_blank">Riverpod 官方文档</a> - Riverpod 状态管理完整学习指南</li>
<li><a href="https://riverpod.dev/" target="_blank">Riverpod 官网</a> - 了解 Riverpod 的核心特性和优势</li>
</ul>
<h3 id="为什么选择-riverpod">为什么选择 Riverpod</h3>
<p>本项目使用 <strong>Riverpod</strong> 作为状态管理方案,基于以下核心技术优势和实践教训:</p>
<h5>1. 性能优化机制</h5>
<h6>刷新颗粒度Rebuild Granularity</h6>
<p><strong>GetX + Obx 的问题:</strong></p>
<ul>
<li>Obx 包裹的整个 Widget 都会重建</li>
<li>嵌套 Obx 会导致多次重建</li>
<li>无法精确控制重建范围</li>
</ul>
<p><strong>实际案例UUTalk</strong></p>
<pre><code class="language-dart">// 整个 Container 都会重建
Obx(() =&gt; Container(
height: controller.height.value, // 高度变化
child: Column(
children: [
Text(controller.title.value), // title 变化也会重建整个 Container
Text(controller.content.value), // content 变化也会重建整个 Container
],
),
))
</code></pre>
<p><strong>Riverpod 的精细化刷新优化:</strong></p>
<table>
<thead>
<tr>
<th>优化技术</th>
<th>作用</th>
<th>使用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>select</strong></td>
<td>精确订阅状态的某个字段</td>
<td>只关心状态中的部分数据,避免整个对象变化导致重建</td>
</tr>
<tr>
<td><strong>Consumer</strong></td>
<td>局部重建 Widget 树的一部分</td>
<td>只重建需要响应状态变化的最小 Widget 子树</td>
</tr>
<tr>
<td><strong>RepaintBoundary</strong></td>
<td>隔离重绘边界</td>
<td>阻止父级重绘影响子级,或子级重绘影响父级</td>
</tr>
<tr>
<td><strong>autoDispose</strong></td>
<td>自动释放不再使用的 Provider</td>
<td>页面销毁时自动清理资源,避免内存泄漏</td>
</tr>
<tr>
<td><strong>family</strong></td>
<td>为不同参数创建独立 Provider 实例</td>
<td>列表中每个 Item 有独立状态,互不影响</td>
</tr>
</tbody>
</table>
<h6>代码示例</h6>
<pre><code class="language-dart">// 1. select只订阅 title 字段content 变化时不重建
final title = ref.watch(chatViewModelProvider.select((s) =&gt; 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&lt;UserViewModel, UserState&gt;(
(ref) =&gt; UserViewModel(),
);
// 5. family列表中每个 Item 有独立状态
final messageProvider = Provider.family&lt;Message, int&gt;((ref, messageId) {
return ref.watch(chatRepositoryProvider).getMessageById(messageId);
});
</code></pre>
<p><strong>性能对比:</strong></p>
<table>
<thead>
<tr>
<th>场景</th>
<th>不使用优化</th>
<th>使用优化</th>
<th>提升</th>
</tr>
</thead>
<tbody>
<tr>
<td>聊天列表滚动</td>
<td>所有 Item 重建</td>
<td>只重建可见 Item</td>
<td>性能显著提升</td>
</tr>
<tr>
<td>状态局部更新</td>
<td>整个页面重建</td>
<td>只重建 Consumer 部分</td>
<td>大幅减少 rebuild</td>
</tr>
<tr>
<td>动画播放</td>
<td>影响整个 Widget 树</td>
<td>RepaintBoundary 隔离</td>
<td>流畅度明显提升</td>
</tr>
<tr>
<td>列表 Item 状态</td>
<td>共享状态,互相影响</td>
<td>family 独立状态</td>
<td>避免无关重建</td>
</tr>
</tbody>
</table>
<p><strong>性能提升效果:</strong></p>
<ul>
<li>大幅减少不必要的 rebuild</li>
<li>明显降低 CPU 使用</li>
<li>显著提升滑动流畅度</li>
</ul>
<h5>2. 单向数据流Unidirectional Data Flow</h5>
<h6>为什么需要单向数据流?</h6>
<p><strong>GetX 的双向绑定问题:</strong></p>
<pre><code class="language-dart">// GetX 允许在任何地方修改状态,导致数据流混乱
class ChatPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find&lt;ChatController&gt;();
return Column(
children: [
// 问题1UI 直接修改状态
ElevatedButton(
onPressed: () =&gt; controller.isLoading.value = true, // UI 直接改状态
),
// 问题2不知道状态从哪里来往哪里去
Obx(() =&gt; Text(controller.message.value)),
],
);
}
}
</code></pre>
<p><strong>数据流混乱导致:</strong></p>
<ul>
<li>无法追踪状态变化来源</li>
<li>多处修改同一状态,冲突频发</li>
<li>Debug 困难,不知道谁改了状态</li>
</ul>
<p><strong>Riverpod 的单向数据流:</strong></p>
<pre><code>用户操作 → ViewModel 方法 → 更新 State → UI 自动响应
↑ ↓
└─────────────── 只读数据 ──────────────────┘
</code></pre>
<p><strong>代码实现:</strong></p>
<pre><code class="language-dart">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: () =&gt; viewModel.sendMessage('hello'),
),
// 响应UI 自动响应状态变化
Text(state.message),
],
);
}
}
</code></pre>
<p><strong>优势:</strong></p>
<ul>
<li>数据流向清晰:用户操作 → ViewModel → State → UI</li>
<li>状态只有一个修改入口ViewModel 的方法</li>
<li>易于追踪DevTools 可以看到完整的状态变化历史</li>
<li>状态历史回溯:可以回退查看之前任意时刻的应用状态,像看视频回放一样</li>
</ul>
<h5>3. 编译期类型安全Compile-time Safety</h5>
<h6>GetX 的运行时陷阱</h6>
<p><strong>问题1字符串查找 Controller</strong></p>
<pre><code class="language-dart">// 编译通过,运行时崩溃
final controller = Get.find&lt;ChatController&gt;(); // 如果忘记 Get.put运行时炸
</code></pre>
<p><strong>问题2依赖关系不明确</strong></p>
<pre><code class="language-dart">class ChatController extends GetxController {
void sendMessage() {
// 不知道依赖了什么,运行时才知道
objectMgr.chatMgr.send(); // objectMgr 是什么?从哪来?
}
}
</code></pre>
<p><strong>问题3类型不安全</strong></p>
<pre><code class="language-dart">// 编译通过,运行时类型错误
Get.put&lt;BaseController&gt;(ChatController());
final controller = Get.find&lt;UserController&gt;(); // 找到 ChatController类型不匹配
</code></pre>
<p><strong>运行时错误案例UUTalk 实际问题):</strong></p>
<ul>
<li>Controller 未初始化</li>
<li>类型转换错误</li>
<li>依赖循环引用</li>
</ul>
<h6>Riverpod 的编译期保证</h6>
<p><strong>保证1依赖必须存在</strong></p>
<pre><code class="language-dart">@riverpod
class ChatViewModel extends _$ChatViewModel {
@override
ChatState build() {
// 编译期就知道依赖了什么
final chatRepo = ref.watch(chatRepositoryProvider); // 如果 Provider 不存在,编译报错
return const ChatState();
}
}
</code></pre>
<p><strong>保证2类型完全正确</strong></p>
<pre><code class="language-dart">// 类型由编译器推断,完全正确
final state = ref.watch(chatViewModelProvider); // state 类型是 ChatState
final viewModel = ref.read(chatViewModelProvider.notifier); // viewModel 类型是 ChatViewModel
</code></pre>
<p><strong>保证3依赖图可视化</strong></p>
<pre><code class="language-dart">// Riverpod DevTools 可以看到完整的依赖图
ChatViewModel
├─ chatRepositoryProvider
│ ├─ apiClientProvider
│ └─ messageLocalDataSourceProvider
└─ sendMessageUseCaseProvider
└─ chatRepositoryProvider
</code></pre>
<p><strong>编译期保证的价值:</strong></p>
<ul>
<li>大幅减少 Bug运行时错误显著下降</li>
<li>提升开发效率:不需要运行才知道对错</li>
<li>重构安全IDE 自动提示依赖变化</li>
<li>团队协作:依赖关系明确,不会互相影响</li>
</ul>
<h4>GetX + Obx vs Riverpod来自 UUTalk 项目的实践教训</h4>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p><strong>真实案例警示</strong></p>
<p>以下内容基于 <strong>UUTalk 项目</strong>的实际经验,展示了 GetX + Obx 在大型项目中暴露的严重问题。</p>
</div>
<h5>1. 状态管理混乱</h5>
<p><strong>UUTalk 项目的 GetX + Obx 问题代码chat_list_controller.dart</strong></p>
<pre><code class="language-dart">class ChatListController extends GetxController {
var lastClickedSpecialChatId = (-1).obs;
final chatList = &lt;Chat&gt;[].obs;
final companyChatList = &lt;Chat&gt;[].obs;
final lockedChatList = &lt;Chat&gt;[].obs;
final RxBool isNavigating = false.obs;
final isInitializing = true.obs;
ValueNotifier&lt;bool&gt; isInitializingValue = ValueNotifier(true); // 混用!
final isShowSkeleton = false.obs;
ValueNotifier&lt;double&gt; 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+ 个响应式变量!
}</code></pre>
<div style="background-color: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin: 15px 0;">
<p><strong>问题分析:</strong></p>
<ul>
<li><strong>状态零散</strong>50+ 个独立的 <code>.obs</code> 变量,没有统一结构</li>
<li><strong>混合使用</strong><code>RxBool</code>, <code>RxDouble</code>, <code>ValueNotifier</code> 混杂</li>
<li><strong>重复状态</strong><code>offset</code><code>offsetObx</code> 表示同一个东西</li>
<li><strong>命名混乱</strong>:英文、拼音、中文注释混杂</li>
<li><strong>UI 状态污染</strong>:动画、透明度、偏移量都混在 Controller 里</li>
</ul>
</div>
<h5>2. 过度嵌套和性能问题</h5>
<p><strong>UUTalk 项目的 Obx 嵌套地狱home_view.dart</strong></p>
<pre><code class="language-dart">body: Obx(() =&gt; AnimatedPadding(
child: GetBuilder(
builder: (_) =&gt; Obx(
() =&gt; Stack(
children: [
Obx(
() =&gt; Container(
child: Obx(
() =&gt; Opacity(
opacity: controller.opacity.value,
child: Obx(
() =&gt; AnimatedContainer(...), // 第6层 Obx
),
),
),
),
),
],
),
),
),
)),</code></pre>
<div style="background-color: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin: 15px 0;">
<p><strong>问题分析:</strong></p>
<ul>
<li><strong>嵌套地狱</strong>:单个文件 12 个 Obx最深 6 层嵌套</li>
<li><strong>过度重建</strong>:每个 Obx 独立监听,导致大量不必要的 rebuild</li>
<li><strong>Obx + GetBuilder 混用</strong>:逻辑混乱,性能更差</li>
<li><strong>难以调试</strong>:无法追踪哪个状态导致了 rebuild</li>
</ul>
</div>
<h5>3. Controller 过于臃肿</h5>
<p><strong>UUTalk 项目的巨型 Controller</strong></p>
<pre><code class="language-dart">// 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&lt;void&gt; loadChats() { ... }
Future&lt;void&gt; sendMessage() { ... }
// 动画逻辑
void startAnimation() { ... }
late AnimationController animController;
// 路由逻辑
void navigateToChat() { ... }
// ... 几千行代码全在一个文件!
}</code></pre>
<div style="background-color: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin: 15px 0;">
<p><strong>问题分析:</strong></p>
<ul>
<li><strong>77 个 import</strong>:依赖关系复杂,难以维护</li>
<li><strong>职责不清</strong>UI、业务、动画、路由全混在一起</li>
<li><strong>文件巨大</strong>:单个 Controller 文件几千行</li>
<li><strong>难以复用</strong>:逻辑耦合,无法独立使用</li>
</ul>
</div>
<h5>4. 没有编译时安全</h5>
<p><strong>UUTalk 项目的运行时陷阱:</strong></p>
<pre><code class="language-dart">// 通过字符串查找 Controller运行时才知道对错
Get.find&lt;ChatListController&gt;();
Get.put(ChatListController()); // 全局单例,容易冲突
// 依赖注入不明确,不知道依赖了什么
class ChatListController extends GetxController {
void loadData() {
// 直接访问全局 Manager依赖不透明
objectMgr.chatMgr.getChats();
objectMgr.groupMgr.getGroups();
// 不知道这个 Controller 到底依赖了什么
}
}</code></pre>
<div style="background-color: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin: 15px 0;">
<p><strong>问题分析:</strong></p>
<ul>
<li><strong>运行时错误</strong>:编译通过,运行时才崩溃</li>
<li><strong>全局污染</strong><code>Get.find</code> 全局查找,容易命名冲突</li>
<li><strong>依赖不明</strong>:不知道 Controller 依赖了哪些服务</li>
<li><strong>难以重构</strong>:改一个地方,不知道影响范围</li>
</ul>
</div>
<h5>5. 难以测试</h5>
<p><strong>UUTalk 项目的测试困境:</strong></p>
<pre><code class="language-dart">// 测试时必须初始化整个 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 是全局单例,无法替换
});</code></pre>
<div style="background-color: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin: 15px 0;">
<p><strong>问题分析:</strong></p>
<ul>
<li><strong>测试困难</strong>:需要初始化整个 GetX 生态</li>
<li><strong>无法 Mock</strong>:依赖全局对象,无法注入 mock</li>
<li><strong>测试耦合</strong>:测试依赖 GetX 框架</li>
<li><strong>覆盖率低</strong>:太难测,团队放弃测试</li>
</ul>
</div>
<h5>GetX + Obx vs Riverpod 实际对比(基于 UUTalk 项目经验)</h5>
<table>
<thead>
<tr>
<th>技术维度</th>
<th>GetX + Obx</th>
<th>Riverpod</th>
<th>技术优势</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>刷新颗粒度</strong></td>
<td>Obx 包裹的整个 Widget<br/>无法精确控制范围</td>
<td>精确依赖追踪<br/>支持 select 细粒度订阅</td>
<td>大幅减少不必要 rebuild</td>
</tr>
<tr>
<td><strong>性能基准</strong></td>
<td>状态更新较慢<br/>内存占用较高</td>
<td>状态更新快速<br/>内存占用较低</td>
<td>性能明显提升<br/>内存显著减少</td>
</tr>
<tr>
<td><strong>数据流</strong></td>
<td>双向绑定<br/>状态可在任意位置修改</td>
<td>单向数据流<br/>状态只能通过 ViewModel 修改</td>
<td>数据流清晰可追踪<br/>便于状态历史回溯</td>
</tr>
<tr>
<td><strong>编译期安全</strong></td>
<td>运行时查找 Controller<br/>类型错误运行时才发现</td>
<td>编译期类型检查<br/>依赖不存在立即报错</td>
<td>大幅减少 Bug<br/>提升开发效率</td>
</tr>
<tr>
<td><strong>代码生成</strong></td>
<td>不支持</td>
<td>riverpod_generator<br/>freezed 全面支持</td>
<td>大幅减少样板代码<br/>显著提升开发效率</td>
</tr>
<tr>
<td><strong>依赖管理</strong></td>
<td>全局 Get.put/find<br/>依赖关系不透明</td>
<td>Provider 依赖图<br/>编译期验证依赖</td>
<td>依赖关系可视化<br/>重构安全有保障</td>
</tr>
<tr>
<td><strong>测试支持</strong></td>
<td>需要初始化 GetX 框架<br/>无法 Mock 全局依赖</td>
<td>Provider 可轻松覆盖<br/>纯函数易于测试</td>
<td>测试更容易<br/>运行速度更快</td>
</tr>
<tr>
<td><strong>DevTools</strong></td>
<td>基础日志</td>
<td>完整依赖图<br/>状态历史回溯<br/>性能分析</td>
<td>Debug 效率显著提升</td>
</tr>
</tbody>
</table>
<h5>真实案例对比</h5>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0;">
<div style="background-color: #f8d7da; border: 2px solid #dc3545; padding: 15px; border-radius: 8px;">
<p><strong>GetX + ObxUUTalk 现状)</strong></p>
<p><strong>状态混乱:</strong></p>
<pre><code class="language-dart">class ChatListController extends GetxController {
final chatList = &lt;Chat&gt;[].obs;
final isLoading = false.obs;
ValueNotifier&lt;double&gt; offset = ValueNotifier(0); // 混用!
RxDouble offsetObx = 0.0.obs; // 重复!
RxBool isShowingApplet = false.obs;
RxBool isMiniAppletDraggingIcon = false.obs;
// ... 50+ 个零散变量
void loadChats() {
objectMgr.chatMgr.getChats(); // 全局依赖,不可测
}
}</code></pre>
<p><strong>UI 嵌套地狱:</strong></p>
<pre><code class="language-dart">Obx(() =&gt; GetBuilder(
builder: (_) =&gt; Obx(
() =&gt; Obx(
() =&gt; Obx(() =&gt; ...), // 6 层嵌套!
),
),
))</code></pre>
</div>
<div style="background-color: #d4edda; border: 2px solid #28a745; padding: 15px; border-radius: 8px;">
<p><strong>Riverpod新架构</strong></p>
<p><strong>状态清晰:</strong></p>
<pre><code class="language-dart">@freezed
class ChatListState with _$ChatListState {
const factory ChatListState({
@Default([]) List&lt;Chat&gt; chats,
@Default(false) bool isLoading,
}) = _ChatListState;
}
class ChatListViewModel extends StateNotifier&lt;ChatListState&gt; {
ChatListViewModel(this._chatRepository)
: super(const ChatListState());
final ChatRepository _chatRepository; // 依赖明确
Future&lt;void&gt; loadChats() async {
state = state.copyWith(isLoading: true);
final chats = await _chatRepository.getChats();
state = state.copyWith(chats: chats, isLoading: false);
}
}</code></pre>
<p><strong>UI 清晰:</strong></p>
<pre><code class="language-dart">class ChatListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(chatListViewModelProvider);
return state.isLoading
? LoadingWidget()
: ChatListView(chats: state.chats);
}
}</code></pre>
</div>
</div>
<h5>UUTalk 项目的痛点总结</h5>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p>从 UUTalk 项目的实际经验中,我们总结出 GetX + Obx 的核心问题:</p>
<ol>
<li><strong>"快速开发"变成"技术债"</strong>:初期确实快,但 6 个月后代码无法维护</li>
<li><strong>"响应式"变成"性能杀手"</strong>Obx 嵌套导致过度重建,页面卡顿</li>
<li><strong>"灵活"变成"混乱"</strong>:没有约束,代码风格千差万别</li>
<li><strong>"全局管理"变成"依赖噩梦"</strong>:牵一发动全身,不敢重构</li>
<li><strong>"简单上手"变成"难以精通"</strong>:团队成员写出的代码质量参差不齐</li>
</ol>
</div>
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
<p><strong>Riverpod 的技术优势</strong></p>
<p>基于 UUTalk 项目的实践教训,我们选择 Riverpod 作为新架构的状态管理方案,因为它从根本上解决了 GetX 的所有问题:</p>
<ul>
<li><strong>编译时安全</strong>:不会再有运行时崩溃</li>
<li><strong>结构化状态</strong>@freezed 强制统一状态结构</li>
<li><strong>精确重建</strong>:依赖追踪精确,性能优异</li>
<li><strong>职责清晰</strong>ViewModel、Repository、Service 分离明确</li>
<li><strong>易于测试</strong>Provider 可轻松覆盖和 Mock</li>
<li><strong>团队规范</strong>:代码风格统一,质量可控</li>
</ul>
</div>
<h4>Riverpod vs Provider 对比</h4>
<table>
<thead>
<tr>
<th>特性</th>
<th>Provider</th>
<th>Riverpod</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>编译时安全</strong></td>
<td>不支持</td>
<td>支持</td>
</tr>
<tr>
<td><strong>需要 BuildContext</strong></td>
<td>需要</td>
<td>不需要</td>
</tr>
<tr>
<td><strong>代码生成</strong></td>
<td>不支持</td>
<td>支持</td>
</tr>
<tr>
<td><strong>测试友好</strong></td>
<td>一般</td>
<td>优秀</td>
</tr>
<tr>
<td><strong>性能</strong></td>
<td></td>
<td>更好</td>
</tr>
<tr>
<td><strong>学习曲线</strong></td>
<td>平缓</td>
<td>稍陡</td>
</tr>
</tbody>
</table>
<h4>MVVM + Riverpod 优势</h4>
<ul>
<li><strong>清晰的数据流动</strong>:数据流向明确,易于追踪</li>
<li><strong>响应式更新</strong>UI 与数据自动同步,无需手动刷新</li>
<li><strong>状态管理</strong>:统一管理应用状态,避免状态混乱</li>
<li><strong>可测试性</strong>ViewModel 可独立测试,无需 Widget 环境</li>
<li><strong>解耦</strong>View 与 Model 完全分离,业务逻辑独立</li>
<li><strong>类型安全</strong>:编译时检查,避免运行时错误</li>
</ul>
<h3 id="feature-驱动开发">Feature 驱动开发</h3>
<h4>以页面为单位</h4>
<p>App 是以页面为导向,设计架构时,必须明确针对平台页面进行开发。每个功能页面独立成一个 Feature。</p>
<h4>完整生命周期</h4>
<p>每个 Feature 包含完整的生命周期:</p>
<div class="mermaid">
flowchart TD
UI[UI Layer<br/>用户界面] --> Presentation[Presentation Layer<br/>视图模型]
Presentation --> Domain[Domain Layer<br/>业务逻辑]
Domain --> Data[Data Layer<br/>数据访问]
Data --> Core[Core Layer<br/>应用级基础设施]
Data --> SDKs[SDK Packages<br/>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
</div>
<h4>高内聚低耦合</h4>
<ul>
<li>Feature 之间通过接口通信</li>
<li>每个 Feature 可独立开发、测试、部署</li>
<li>Feature 内部高度内聚,外部低耦合</li>
<li>便于团队并行开发</li>
</ul>
<h3 id="模块设计哲学">模块设计哲学</h3>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<h4 style="margin-top: 0; color: #388e3c;">核心设计原则</h4>
<blockquote style="background: transparent; border: none; padding: 0; margin: 10px 0;">
<p style="font-size: 18px; font-weight: 600; color: #2e7d32;">"实现层高度封装,使用侧傻瓜式"</p>
</blockquote>
<p>这是本架构所有模块设计遵循的核心哲学:</p>
<ul style="margin-bottom: 0;">
<li><strong>实现层高度封装</strong>:将复杂的技术细节、错误处理、类型转换等全部封装在底层</li>
<li><strong>使用侧傻瓜式</strong>:上层使用者只需关注业务逻辑,无需了解底层实现细节</li>
<li><strong>按需使用</strong>:提供合理的默认值和可选参数,使用者可以按需定制</li>
</ul>
</div>
<h4>网络层设计示例</h4>
<p>以网络层为例,展示如何实现"实现层高度封装,使用侧傻瓜式"的设计原则。</p>
<h5>1. APIRequestable 协议 - 统一请求接口</h5>
<p><strong>设计思想</strong>:定义统一的 API 请求协议,所有请求都实现相同的接口</p>
<pre><code class="language-dart">/// API 请求协议 - 所有请求的基础接口
/// parameters 自动序列化
abstract class APIRequestable&lt;T&gt; {
/// API 路径
String get path;
/// HTTP 方法
HTTPMethod get method;
/// 序列化为 JSON由 @JsonSerializable 自动生成)
Map&lt;String, dynamic&gt; toJson();
/// 自定义请求头
Map&lt;String, String&gt;? get customHeaders =&gt; null;
/// 请求类型(决定 header 处理方式)
APIRequestType get requestType =&gt; APIRequestType.request;
/// 解码响应(默认实现由扩展提供)
T? decodeResponse(Response response);
}
/// 默认实现 - parameters 自动调用 toJson()
extension APIRequestableDefaults&lt;T&gt; on APIRequestable&lt;T&gt; {
/// 请求参数(自动序列化,用户无需手动定义)
Map&lt;String, dynamic&gt;? get parameters {
// 对于 upload 类型,不序列化参数
if (requestType == APIRequestType.upload) {
return null;
}
// 自动调用 toJson() 序列化请求对象
return toJson();
}
}
</code></pre>
<p><strong>核心优势</strong></p>
<ul>
<li><strong>自动序列化</strong>parameters 自动调用 toJson(),用户无需手动定义</li>
<li><strong>统一接口</strong>:所有 API 请求都实现同一个协议</li>
<li><strong>类型安全</strong>:泛型 T 指定响应数据类型</li>
<li><strong>注解驱动</strong>:通过注解自动生成代码</li>
</ul>
<h5>2. 统一执行入口 - executeRequest</h5>
<p><strong>设计思想</strong>:提供唯一的请求执行入口,自动处理所有技术细节</p>
<pre><code class="language-dart">/// 执行 API 请求 - 唯一的请求入口
Future&lt;T?&gt; executeRequest&lt;T&gt;(Ref ref, APIRequestable&lt;T&gt; request) async {
final dio = ref.read(apiClientProvider);
final config = ref.read(aPIConfigurationProvider);
// 1. 检查网络连接
if (!networkManager.isNetworkAvailable) {
throw const APIError.noNetworkConnection();
}
try {
// 2. 根据请求类型构建 header
final headers = configNotifier.defaultHeaders(
customHeaders: request.customHeaders,
includeToken: request.requestType != APIRequestType.login,
);
// 3. 执行请求
final response = await dio.request(
'${config.baseURL}${request.path}',
data: request.parameters,
options: Options(method: request.method.value, headers: headers),
);
// 4. 自动解码响应
return request.decodeResponse(response);
} on DioException catch (e) {
// 5. 统一错误处理
throw _handleDioError(e);
}
}
</code></pre>
<p><strong>封装的技术细节</strong></p>
<ol>
<li>网络可用性检查</li>
<li>请求头自动构建Token、Content-Type 等)</li>
<li>URL 拼接</li>
<li>响应自动解码</li>
<li>错误统一处理和转换</li>
</ol>
<h5>3. 自动响应解码 - 注册机制</h5>
<p><strong>设计思想</strong>:通过注册机制,自动查找 fromJson 函数,实现响应的自动解码</p>
<pre><code class="language-dart">/// 响应类型注册表
final _fromJsonRegistry = &lt;Type, Function&gt;{};
/// 注册响应类型 - 一次注册,全局可用
T Function(Map&lt;String, dynamic&gt;)? registerResponse&lt;T&gt;(
T Function(Map&lt;String, dynamic&gt;) fromJson,
) {
_fromJsonRegistry[T] = fromJson;
return fromJson;
}
/// 自动解码扩展 - 使用侧无需关心
extension APIRequestableExtension&lt;T&gt; on APIRequestable&lt;T&gt; {
T? decodeResponse(Response response) {
final data = response.data as Map&lt;String, dynamic&gt;;
// 从注册表查找 fromJson 函数
final fromJsonFunc = _fromJsonRegistry[T] as T Function(Map&lt;String, dynamic&gt;)?;
if (fromJsonFunc == null) {
throw StateError('fromJson not registered for type $T');
}
// 自动解码 APIResponseWrapper
final wrapper = APIResponseWrapper&lt;T&gt;.fromJson(
data,
(json) =&gt; fromJsonFunc(json as Map&lt;String, dynamic&gt;),
);
// 检查业务错误码
if (wrapper.code != 0) {
throw APIError.apiError(code: wrapper.code, message: wrapper.message);
}
return wrapper.data;
}
}
</code></pre>
<p><strong>使用示例</strong></p>
<p>一个端点 = 一个文件(<code>data/remote/login_request.dart</code>Response DTO + Request 放在同一文件中。</p>
<pre><code class="language-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;
@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&lt;String, dynamic&gt; json) =&gt; _$LoginDataFromJson(json);
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginDataToJson(this);
User toEntity() =&gt; User(id: userId, email: email); // DTO → Domain Entity
}
// ── Request ──
// @ApiRequest 自动生成 path / method / requestType / includeToken / fromJson 注册
@ApiRequest(
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
method: HttpMethod.post,
responseType: LoginData,
requestType: ApiRequestType.login,
)
@JsonSerializable()
class LoginRequest extends ApiRequestable&lt;LoginData&gt; with _$LoginRequestApi {
final String email;
final String password;
LoginRequest({required this.email, required this.password});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginRequestToJson(this);
}
</code></pre>
<pre><code class="language-dart">// 使用 - 超级简单!
final loginData = await apiClient.executeRequest(
LoginRequest(email: 'user@example.com', password: '123456'),
);
final user = loginData?.toEntity(); // DTO → Domain Entity
</code></pre>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<h5 style="margin-top: 0; color: #388e3c;">设计思想对比</h5>
<table>
<thead>
<tr>
<th>方案</th>
<th>需要定义的内容</th>
<th>自动生成</th>
<th>维护成本</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>手动方式</strong></td>
<td>字段 + 构造函数 + extends + path + method + toJson + parameters + registerResponse</td>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>JsonSerializable</strong></td>
<td>字段 + 构造函数 + extends + path + method</td>
<td>toJson / fromJson</td>
<td></td>
</tr>
<tr>
<td><strong>@ApiRequest + @JsonSerializable</strong></td>
<td>字段 + 构造函数 + @ApiRequest + @JsonSerializable</td>
<td>path / method / requestType / includeToken / toJson / fromJson / fromJson 注册</td>
<td><strong></strong></td>
</tr>
</tbody>
</table>
<p><strong>核心优势</strong></p>
<ul>
<li><strong>注解驱动</strong><code>@ApiRequest</code> 自动生成 mixin<code>@JsonSerializable</code> 自动生成 toJson/fromJson</li>
<li><strong>自动注册</strong>fromJson 在首次请求时自动注册到全局注册表,无需手动 <code>registerApiResponses()</code></li>
<li><strong>一个端点 = 一个文件</strong>Response DTO + Request 放在同一文件,打开即看全貌</li>
<li><strong>傻瓜式使用</strong>:使用者只需关注业务字段和注解配置</li>
<li><strong>类型安全</strong><code>ApiRequestable&lt;T&gt;</code> 泛型 + <code>responseType</code> 编译期检查</li>
</ul>
<p><strong>跨平台对比</strong></p>
<table>
<thead>
<tr>
<th>平台</th>
<th>代码示例</th>
<th>简洁度</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Swift</strong></td>
<td><code>struct LoginRequest: APIRequestable { typealias Response = LoginData ... }</code></td>
<td>协议直接实现,最简洁</td>
</tr>
<tr>
<td><strong>Dart</strong></td>
<td><code>@ApiRequest(...) class LoginRequest extends ApiRequestable&lt;LoginData&gt; with _$LoginRequestApi { ... }</code></td>
<td>注解 + 代码生成,接近 Swift 体验</td>
</tr>
</tbody>
</table>
<p style="color: #2e7d32; font-weight: 600; margin-top: 15px;">
Dart 通过注解 + 代码生成弥补语言层面没有协议默认实现的不足,达到接近 Swift 的简洁度。
</p>
</div>
<h5>4. 注解定义与代码生成器</h5>
<p><strong>设计思想</strong>:通过注解 + 代码生成器,自动生成所有技术代码</p>
<h6>4.1 注解定义</h6>
<p><strong>文件:<code>packages/networks_sdk/lib/src/annotations/api_request.dart</code></strong></p>
<pre><code class="language-dart">/// API 请求注解 - 标记一个类为 API 请求
///
/// 代码生成器会自动生成 mixin `_$<ClassName>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&lt;String, String&gt;? customHeaders;
const ApiRequest({
required this.path,
this.method = HttpMethod.post,
required this.responseType,
this.requestType = ApiRequestType.request,
this.includeToken,
this.customHeaders,
});
}
</code></pre>
<h6>4.2 代码生成器核心逻辑</h6>
<p><strong>文件:<code>packages/networks_sdk/lib/src/generator/api_request_generator.dart</code></strong></p>
<p>生成 <strong>mixin</strong>(非 extension因为 mixin 可以 override 基类方法、调用 <code>super</code>,并在 <code>parameters</code> getter 中自动注册 fromJson。</p>
<pre><code class="language-dart">/// API 请求代码生成器
class ApiRequestGenerator extends GeneratorForAnnotation&lt;ApiRequest&gt; {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
final className = element.name;
final path = annotation.read('path').stringValue;
final methodName = _readEnumName(annotation.read('method').objectValue, 'post');
final responseType = annotation.read('responseType').typeValue;
final responseTypeName = responseType.getDisplayString();
final requestTypeName = _readEnumName(annotation.read('requestType').objectValue, 'request');
// includeToken默认 login → false其余 → true
final includeTokenReader = annotation.peek('includeToken');
final includeToken = (includeTokenReader != null &amp;&amp; !includeTokenReader.isNull)
? includeTokenReader.boolValue
: requestTypeName != 'login';
// 生成 mixin使用侧只需 `with _$XxxApi`
return '''
/// Generated by @ApiRequest for [$className]
mixin _\$${className}Api on ApiRequestable&lt;$responseTypeName&gt; {
@override String get path =&gt; '$path';
@override HttpMethod get method =&gt; HttpMethod.$methodName;
@override ApiRequestType get requestType =&gt; ApiRequestType.$requestTypeName;
@override bool get includeToken =&gt; $includeToken;
@override
Map&lt;String, dynamic&gt;? get parameters {
registerResponse&lt;$responseTypeName&gt;($responseTypeName.fromJson);
return super.parameters;
}
}
''';
}
}
</code></pre>
<p><strong>关键设计</strong><code>parameters</code> getter 在首次请求时自动调用 <code>registerResponse</code>,将 <code>fromJson</code> 注册到全局注册表。无需手动注册,也无需 <code>registerApiResponses()</code> 启动函数。</p>
<h6>4.3 build.yaml 配置</h6>
<p><strong>文件:<code>packages/networks_sdk/build.yaml</code></strong></p>
<p>使用 <code>SharedPartBuilder</code>,与 <code>@JsonSerializable</code> 共享同一个 <code>.g.dart</code> 文件,无需额外 part 指令。</p>
<pre><code class="language-yaml">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"]
</code></pre>
<h6>4.4 运行命令</h6>
<div style="background: #e8f5e9; padding: 15px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 10px 0;">
<p style="margin-top: 0; font-weight: 700; color: #388e3c;">⚠️ 强制要求:开发期间必须常驻 watch 模式</p>
<pre><code class="language-bash"># 在项目根目录打开一个独立终端窗口,执行(整个开发期间只需一次):
melos run gen:watch
</code></pre>
<p style="margin-bottom: 0;">启动后,<strong>每次保存 .dart 文件都会自动重新生成 .g.dart</strong><br/>
手写代码时 IDE 报红是正常的,保存后红线自动消失。</p>
</div>
<pre><code class="language-bash"># 仅在 watch 出问题时使用:全量重新生成
melos run gen
</code></pre>
<h6>4.5 更多使用示例</h6>
<p>所有示例遵循同一模式:<code>@ApiRequest</code> + <code>@JsonSerializable</code> + <code>extends ApiRequestable&lt;T&gt; with _$XxxApi</code></p>
<p><strong>发送消息请求POST</strong></p>
<pre><code class="language-dart">// data/remote/send_message_request.dart
// ── Response DTO ──
@JsonSerializable()
class SendMessageData {
@JsonKey(name: 'message_id')
final String messageId;
final int timestamp;
const SendMessageData({required this.messageId, required this.timestamp});
factory SendMessageData.fromJson(Map&lt;String, dynamic&gt; json) =&gt;
_$SendMessageDataFromJson(json);
}
// ── Request ──
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
@JsonSerializable()
class SendMessageRequest extends ApiRequestable&lt;SendMessageData&gt;
with _$SendMessageRequestApi {
@JsonKey(name: 'chat_id')
final String chatId;
final String content;
SendMessageRequest({required this.chatId, required this.content});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$SendMessageRequestToJson(this);
}
</code></pre>
<p><strong>获取用户资料GET靠 token 标识当前用户,无需传参):</strong></p>
<pre><code class="language-dart">// data/remote/get_profile_request.dart
@JsonSerializable()
class ProfileData {
@JsonKey(name: 'user_id')
final String userId;
final String email;
final String? nickname;
final String? avatar;
const ProfileData({required this.userId, required this.email, this.nickname, this.avatar});
factory ProfileData.fromJson(Map&lt;String, dynamic&gt; json) =&gt;
_$ProfileDataFromJson(json);
User toEntity() =&gt; User(id: userId, email: email, nickname: nickname, avatar: avatar);
}
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
@JsonSerializable()
class GetProfileRequest extends ApiRequestable&lt;ProfileData&gt;
with _$GetProfileRequestApi {
GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$GetProfileRequestToJson(this);
}
</code></pre>
<p><strong>上传文件请求FormData multipart</strong></p>
<pre><code class="language-dart">// 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&lt;String, dynamic&gt; json) =&gt;
_$UploadResultFromJson(json);
}
@ApiRequest(
path: ApiPaths.uploadFile,
method: HttpMethod.post,
responseType: UploadResult,
requestType: ApiRequestType.upload,
)
class UploadFileRequest extends ApiRequestable&lt;UploadResult&gt;
with _$UploadFileRequestApi {
final String filePath;
final String? fileName;
UploadFileRequest({required this.filePath, this.fileName});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; {}; // upload 不走 toJson
@override
Object? get uploadData =&gt; FormData.fromMap({
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
});
}
</code></pre>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p><strong>核心价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>极简使用</strong>:字段 + 构造函数 + <code>@ApiRequest</code> + <code>@JsonSerializable</code></li>
<li><strong>零维护</strong>path / method / requestType / includeToken / fromJson 注册 全部自动生成</li>
<li><strong>类型安全</strong>:泛型 <code>ApiRequestable&lt;T&gt;</code> + <code>responseType</code> 编译期检查</li>
<li><strong>一个端点 = 一个文件</strong>Response DTO + Request 放在同一文件,打开即看全貌</li>
</ul>
</div>
<h5>5. Riverpod 集成 - 依赖注入</h5>
<p><strong>设计思想</strong>Network SDK 本身零 Flutter / 零 Riverpod 依赖。App 层通过 Provider 包装,实现全局单例 + 依赖注入。</p>
<p><strong>DI 装配</strong></p>
<pre><code class="language-dart">// ── app/di/network_provider.dart ── SDK 基础设施,全局唯一)
/// 1. API 配置baseURL 来自 config.json → --dart-define-from-file
final apiConfigProvider = Provider&lt;ApiConfig&gt;((ref) {
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {'Platform': 'Android', 'client-version': '1.0.0'},
tokenExpiredCodes: {30002, 30003, 30124},
forceLogoutCodes: {30125},
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
onTokenRefresh: () async { /* 刷新 token */ return null; },
onLog: (message, {tag}) { print('[$tag] $message'); },
);
});
/// 2. API 客户端(内部自动挂载 Auth / Retry / Logging 拦截器)
final apiClientProvider = Provider&lt;ApiClient&gt;((ref) {
return ApiClient(config: ref.read(apiConfigProvider));
});
// ── features/auth/di/auth_providers.dart ── Auth 模块完整 DI 链路)
/// 3. Repository注入 domain 接口类型ViewModel 不感知具体实现)
final authRepositoryProvider = Provider&lt;AuthRepository&gt;((ref) {
final apiConfig = ref.read(apiConfigProvider);
return AuthRepositoryImpl(
client: ref.read(apiClientProvider), // 直接注入 ApiClient
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存networks_sdk
// secureStorage.saveToken(token); // 持久化storage_sdk待接入
},
);
});
/// 4. UseCase按需
final loginUseCaseProvider = Provider&lt;LoginUseCase&gt;((ref) {
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});
</code></pre>
<p><strong>ViewModel 中使用</strong></p>
<pre><code class="language-dart">@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() =&gt; const LoginState();
Future&lt;void&gt; 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);
}
}
}
</code></pre>
<p><strong>完整装配链路</strong></p>
<pre><code>View: ref.watch(loginViewModelProvider)
→ ViewModel: ref.read(loginUseCaseProvider).execute(...)
→ LoginUseCase: 格式校验(邮箱 + 密码)
→ LoginUseCase: authRepository.login(...)
→ Repository: _client.executeRequest(LoginRequest(...))
→ ApiClient.executeRequest() ← networks_sdk 内部
→ AuthInterceptor ← 注入 token + headers
→ Dio.request(baseURL + path, data) ← 实际 HTTP 请求
→ RetryInterceptor ← token 过期自动刷新重试
→ LoggingInterceptor ← 请求/响应日志
← request.decodeResponse(response) ← 自动解码
← ApiResponseWrapper.fromJson ← 拆 { code, msg, data }
← fromJsonRegistry[LoginData] ← 查注册表
← LoginData.fromJson(data) ← 反序列化
← LoginDataDTO
→ onTokenUpdate(token) ← 回调写入 Token内存 + 持久化)
← loginData.toEntity() → User ← DTO → Domain Entity
← User
← state.copyWith(user: user) ← 更新状态
View: ref.watch → 自动 rebuild ← UI 刷新
</code></pre>
<h4>设计哲学在本架构中的体现</h4>
<table>
<thead>
<tr>
<th>层级</th>
<th>实现层(高度封装)</th>
<th>使用侧(傻瓜式)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>网络层</strong></td>
<td>executeRequest 封装所有细节<br/>自动 header、解码、错误处理</td>
<td>定义 Request 类<br/>只需 3 个字段path、method、parameters</td>
</tr>
<tr>
<td><strong>数据层</strong></td>
<td>Repository 封装数据源切换<br/>自动缓存、错误转换</td>
<td>调用 Repository 方法<br/>返回 Domain 模型,无需关心数据来源</td>
</tr>
<tr>
<td><strong>业务层</strong></td>
<td>UseCase 封装业务逻辑<br/>单一职责,可组合</td>
<td>ViewModel 调用 UseCase<br/>专注状态管理,不关心业务细节</td>
</tr>
<tr>
<td><strong>UI层</strong></td>
<td>ViewModel 提供响应式状态<br/>自动重建、错误处理</td>
<td>Widget 监听 Provider<br/>只需 ref.watch无需手动管理状态</td>
</tr>
</tbody>
</table>
<h4>核心优势</h4>
<ul>
<li><strong>降低认知负担</strong>:使用者无需了解底层实现,专注业务逻辑</li>
<li><strong>减少重复代码</strong>:通用逻辑封装在底层,上层不重复</li>
<li><strong>提升可维护性</strong>:修改底层实现不影响上层代码</li>
<li><strong>易于测试</strong>:每层职责清晰,可独立 Mock 和测试</li>
<li><strong>团队协作</strong>:新人快速上手,代码风格统一</li>
</ul>
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
<p><strong>设计哲学总结</strong></p>
<p>本架构中的每一个模块都遵循"实现层高度封装,使用侧傻瓜式"的原则:</p>
<ul style="margin-bottom: 0;">
<li>底层模块负责<strong>技术复杂性</strong>,提供简洁的 API</li>
<li>上层模块负责<strong>业务逻辑</strong>,使用简单的接口</li>
<li>通过这种分层,实现<strong>关注点分离</strong>,让每个开发者专注于自己擅长的领域</li>
</ul>
</div>
<hr>
<h2 id="part2-structure" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第二部分结构是什么Structure- 整体架构</h2>
<h2 id="设计原则">设计原则</h2>
<h3 id="solid-原则">1.1 SOLID 原则</h3>
<table>
<thead><tr>
<th>原则</th>
<th>说明</th>
<th>体现</th>
</tr></thead>
<tbody>
<tr>
<td><strong>单一职责 (SRP)</strong></td>
<td>一个模块只负责一项职责</td>
<td>每个 UseCase 只处理一个业务场景</td>
</tr>
<tr>
<td><strong>开闭原则 (OCP)</strong></td>
<td>对扩展开放,对修改关闭</td>
<td>通过接口、策略模式实现扩展点</td>
</tr>
<tr>
<td><strong>里氏替换 (LSP)</strong></td>
<td>子类可替换父类</td>
<td>所有 Repository 实现可互换</td>
</tr>
<tr>
<td><strong>接口隔离 (ISP)</strong></td>
<td>客户端不依赖不需要的接口</td>
<td>Repository 按功能拆分,不做大而全接口</td>
</tr>
<tr>
<td><strong>依赖倒置 (DIP)</strong></td>
<td>依赖抽象而非具体实现</td>
<td>Domain 层定义接口Data 层实现</td>
</tr>
</tbody></table>
<h3 id="分层依赖规则">1.2 分层依赖规则</h3>
<div class="mermaid">
flowchart TD
UI[UI Layer<br/>界面层]
Presentation[Presentation Layer<br/>表现层]
Domain[Domain Layer<br/>Domain 层]
Data[Data Layer<br/>数据层]
Core[Core Layer<br/>应用级基础设施]
SDKs[SDK Packages<br/>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
</div>
<p><strong>依赖方向</strong>:单向向下,严格禁止反向依赖和跨层调用</p>
<p><strong>严格规则</strong></p>
<ul>
<li>UI 层只能调用 Presentation 层</li>
<li>Presentation 层只能调用 Domain 层</li>
<li>Domain 层定义接口,不依赖具体实现</li>
<li>Data 层实现 Domain 接口,调用 Core 层和 SDK Packages</li>
<li>Core 层提供应用级基础设施SDK Packages 提供可复用技术能力,均不依赖任何上层</li>
</ul>
<h3 id="模块化原则">1.3 模块化原则</h3>
<ul>
<li><strong>高内聚</strong>:相关功能聚合在同一模块</li>
<li><strong>低耦合</strong>:模块间通过接口通信</li>
<li><strong>可替换</strong>:底层实现可替换,上层不受影响</li>
<li><strong>可测试</strong>:每层可独立测试</li>
</ul>
<hr>
<hr>
<h2 id="整体架构">整体架构3图</h2>
<h3 id="2-1-整体模块图">2.1 整体模块图</h3>
<h4>图表说明</h4>
<p>下图展示基于 Feature 驱动的整体模块划分,每个 Feature 包含完整的 UI → Presentation → Domain 层级。</p>
<div class="mermaid">
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<br/>Flutter Plugin]
L10nPkg[L10nSDK]
Crypto[CryptoSDK<br/>占位]
OtherSDK[Media/RTC/Push/Protocol]
end
subgraph Core[Core Layer - 主 App 内部]
subgraph Foundation[core/foundation/ - 应用级基础设施]
Utils[Constants / Config / Errors<br/>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
</div>
<h3 id="2-2-整体目录图">2.2 整体目录图</h3>
<h4>图表说明</h4>
<p>完整的项目目录结构,展示了 Feature 驱动的组织方式和清晰的层级关系。</p>
<pre><code>lib/
├── main.dart # 应用入口:调用 bootstrap(),不含任何业务逻辑
├── app/ # 应用壳(组合根):负责拼装所有模块,禁止在此写业务逻辑
│ ├── app.dart # MaterialApp 根组件 + WidgetsBindingObserver前后台事件
│ ├── bootstrap.dart # 启动入口ProviderScope 包裹 + 依赖初始化
│ │
│ ├── router/ # 路由管理go_router
│ │ ├── app_router.dart # routerProviderStatefulShellRoute + 全局 redirect
│ │ ├── app_route_name.dart # AppRouteName 枚举,路径常量 + fromPath()
│ │ └── guards/
│ │ └── auth_guard.dart # 登录守卫switch AppRouteName穷举防漏路由
│ │
│ └── di/ # 全局 DI — 手动装配的 Provider
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + ApiClient + SocketConfig + SocketClient + SocketManager
│ └── app_providers.dart # 全局共享状态themeModeProvider + AuthNotifier
├── features/ # 功能模块垂直切片Feature 间禁止直接 import
│ │
│ ├── app_tab/ # Tab 容器(底部导航栏)
│ │ └── view/
│ │ └── app_tab.dart # StatefulShellRoute 子壳 + 底部导航逻辑
│ │
│ ├── login/ # 登录 ── 已实现
│ │ ├── di/
│ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider
│ │ ├── presentation/
│ │ │ ├── login_view_model.dart # @riverpod ViewModel生成 login_view_model.g.dart
│ │ │ └── login_state.dart # @freezed State生成 login_state.freezed.dart
│ │ ├── usecases/
│ │ │ └── login_usecase.dart # 格式校验 → Repository → User Entity
│ │ └── view/
│ │ └── login_page.dart # 登录页
│ │
│ ├── chat/ # 聊天 ── 开发中
│ │ ├── presentation/
│ │ │ └── chat_view_model.dart # @riverpod ViewModel
│ │ └── view/
│ │ ├── chat_page.dart # 会话列表页Tab 1
│ │ └── chat_detail_page.dart # 聊天详情页
│ │
│ ├── contact/ # 通讯录 ── 骨架
│ │ └── view/
│ │ └── contact_page.dart # 通讯录页Tab 2
│ │
│ └── settings/ # 设置 ── 已实现(主题切换)
│ ├── di/
│ │ └── settings_providers.dart # settingsRepositoryProvider待 storage_sdk 接入)
│ ├── presentation/
│ │ └── theme_view_model.dart # @riverpod ViewModel生成 theme_view_model.g.dart
│ ├── usecases/
│ │ └── set_theme_usecase.dart # 主题切换用例
│ └── view/
│ ├── settings_page.dart # 设置主页Tab 3
│ ├── theme_view.dart # 主题选择页
│ └── widgets/
│ ├── settings_section_header.dart
│ └── theme_option_tile.dart
├── domain/ # Domain 层(纯 Dart零 Flutter / 零网络依赖)
│ ├── entities/
│ │ └── user.dart # 用户实体
│ │ # message / conversation / contact 待开发
│ └── repositories/
│ └── auth_repository.dart # abstract interface
│ # message / chat / contact_repository 待开发
├── data/ # Data 层implements domain 接口)
│ ├── repositories/
│ │ └── auth_repository_impl.dart # 认证仓库
│ │ # message / chat / contact 待开发
│ ├── local/
│ │ └── drift/ # Drift 本地数据库
│ │ ├── app_database.dart # @DriftDatabase 定义 + onUpgrade 自动补列
│ │ # database_connection.dart 已迁移至 storage_sdk数据库生命周期统一在 SDK 层管理)
│ │ ├── mapper/
│ │ │ └── drift_path_mapper.dart # Drift 路径映射工具
│ │ └── tables/
│ │ └── users.dart # Users 表定义
│ ├── remote/ # Request 文件(一个端点一个文件)
│ │ ├── login_request.dart # 登录
│ │ ├── logout_request.dart # 登出
│ │ ├── get_profile_request.dart # 获取用户信息
│ │ └── upload_file_request.dart # 文件上传
│ │ # send_message / 其他业务端点 待开发
│ └── models/ # 持久化 DTO@JsonSerializable
│ └── user_dto.dart # 用户持久化 DTO
│ # message / conversation / contact_dto 待开发
└── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
├── foundation/ # 基础配置(各为单独文件,非子目录)
│ ├── api_paths.dart # API 路径常量ApiPaths.authLogin 等)
│ ├── config.dart # 运行时配置AppConfig通过 --dart-define-from-file 注入)
│ ├── constants.dart # 全局常量AppConstants重试次数 / 退避延迟 / 超时等)
│ ├── errors.dart # 统一异常体系
│ ├── extensions.dart # Dart 扩展方法String / DateTime / List 等)
│ ├── logger.dart # 日志门面(分级日志,生产环境自动关闭 debug
│ ├── types.dart # 通用类型Result&lt;T&gt; / typedefs / Unit
│ └── utils.dart # 工具函数(纯函数,无副作用)
├── services/ # 跨模块服务(有状态,作为独立 Provider
│ ├── app_initializer.dart # 启动初始化编排(按序初始化各依赖)
│ ├── network_backoff_debouncer.dart # 网络恢复退避防抖4s→8s→...→60s2min 重置)
│ ├── network_monitor.dart # 网络状态监听connectivity_plus
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
└── ui/ # Core UI设计系统 + 可复用组件)
├── base/ # 设计 Token
│ ├── app_theme.dart # ThemeData 组装Light / Dark
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
│ ├── context_theme_ext.dart # BuildContext 主题扩展context.theme / context.colors
│ └── font.dart # 字体TextStyle 定义 + textTheme(brightness)
├── components/ # 原子组件
│ └── app_button.dart # 按钮
│ # app_text_field / app_avatar / app_badge 等 待开发
└── composites/ # 组合组件(目录预留,待开发)
# app_dialog / app_toast / app_empty_state 等
</code></pre>
<h3 id="2-3-整体分层图">2.3 整体分层图MVVM + Riverpod 数据流)</h3>
<h4>图表说明</h4>
<p>展示完整的五层架构,标注 MVVM 角色映射和 Riverpod 驱动的单向数据流。</p>
<div class="mermaid">
flowchart TD
subgraph Layer5["View 层MVVM 的 V"]
direction TB
Pages["ConsumerWidget 页面<br/>ref.watch(viewModel) 订阅状态"]
Widgets["UI Widgets<br/>纯展示组件"]
end
subgraph Layer4["ViewModel 层MVVM 的 VM"]
direction TB
ViewModels["Notifier&lt;State&gt;<br/>状态管理 + 直接方法调用"]
end
subgraph Layer3[Domain 层]
direction TB
UseCases["Use Cases<br/>业务用例"]
Entities["Entities<br/>Domain 实体"]
RepoInterfaces["Repository 接口<br/>(依赖倒置)"]
end
subgraph Layer2[Data 层]
direction TB
RepoImpls[Repository 实现]
LocalDS[Local DataSource]
DTOs["DTO ModelsMVVM 的 M"]
end
subgraph Layer1[Core 层 - 主 App 内部]
direction TB
Foundation[foundation/<br/>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<br/>media_sdk / rtc_sdk / notification_sdk<br/>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
</div>
<h3 id="2-4-mvvm-riverpod-数据流">2.4 MVVM + Riverpod 数据流映射</h3>
<p>目录结构到 MVVM 和 Riverpod 数据流的精确映射:</p>
<div class="mermaid">
flowchart LR
subgraph MVVM["MVVM 角色映射"]
direction TB
V["<b>View</b><br/>view/ 目录<br/>ConsumerWidget"]
VM["<b>ViewModel</b><br/>presentation/ 目录<br/>StateNotifier"]
M["<b>Model</b><br/>model/ + domain/entities/<br/>UI Model + Entity"]
end
subgraph RiverpodFlow["Riverpod 单向数据流"]
direction TB
Step1["① 用户点击发送按钮"]
Step2["② ref.read(chatVM.notifier)<br/>.sendMessage(content)"]
Step3["③ ViewModel 调用 UseCase<br/>→ Repository → ApiClient"]
Step4["④ state = state.copyWith(<br/>messages: [..., newMsg])"]
Step5["⑤ ref.watch(chatVM) 检测变化<br/>→ 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
</div>
<blockquote>
<p><strong>两大核心逻辑</strong></p>
<p>1. <strong>MVVM 分层职责</strong>Viewview/只负责渲染和用户交互ViewModelpresentation/持有状态并处理业务逻辑Modelmodel/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
<p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code><code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动UI 永远是状态的函数。</p>
</blockquote>
<hr>
<hr>
<h2 id="part3-concepts" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第三部分核心概念Core Concepts</h2>
<h2 id="riverpod-核心概念">Riverpod 核心概念</h2>
<p>在深入了解各层实现之前,先理解 Riverpod 的核心概念和使用方式。</p>
<table>
<thead>
<tr>
<th>概念</th>
<th>说明</th>
<th>使用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>StateNotifier</strong></td>
<td>管理可变状态的类</td>
<td>ViewModel 实现</td>
</tr>
<tr>
<td><strong>StateNotifierProvider</strong></td>
<td>提供 StateNotifier 的 Provider</td>
<td>ViewModel Provider</td>
</tr>
<tr>
<td><strong>Provider</strong></td>
<td>提供不可变对象的 Provider</td>
<td>UseCase、Repository 依赖注入</td>
</tr>
<tr>
<td><strong>ConsumerWidget</strong></td>
<td>可以监听 Provider 的 Widget</td>
<td>UI 层页面组件</td>
</tr>
<tr>
<td><strong>WidgetRef</strong></td>
<td>访问 Provider 的引用</td>
<td>在 Widget 中读取和监听 Provider</td>
</tr>
<tr>
<td><strong>@riverpod</strong></td>
<td>代码生成注解</td>
<td>自动生成 Provider 代码</td>
</tr>
<tr>
<td><strong>@freezed</strong></td>
<td>不可变类注解</td>
<td>生成 State 类的 copyWith 等方法</td>
</tr>
<tr>
<td><strong>autoDispose</strong></td>
<td>自动释放 Provider</td>
<td>页面销毁时自动清理资源</td>
</tr>
</tbody>
</table>
<h2 id="clean-architecture-分层">Clean Architecture 分层说明</h2>
<p>本架构严格遵循 Clean Architecture 的分层原则,每层都有明确的职责和依赖方向。</p>
<h3>分层职责</h3>
<table>
<thead>
<tr>
<th>层级</th>
<th>职责</th>
<th>依赖方向</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>UI Layer</strong></td>
<td>界面展示、用户交互</td>
<td>→ Presentation</td>
<td>ChatPage、MessageItem Widget</td>
</tr>
<tr>
<td><strong>Presentation Layer</strong></td>
<td>状态管理、UI 逻辑</td>
<td>→ Domain</td>
<td>ChatViewModel、MessageState</td>
</tr>
<tr>
<td><strong>Domain Layer</strong></td>
<td>业务逻辑、业务规则</td>
<td>→ Repository 接口</td>
<td>SendMessageUseCase、ChatEntity</td>
</tr>
<tr>
<td><strong>Data Layer</strong></td>
<td>数据访问、数据源管理</td>
<td>→ Core</td>
<td>ChatRepository、ChatRepositoryImpl</td>
</tr>
<tr>
<td><strong>Core Layer</strong></td>
<td>应用级基础设施Constants/Config/Errors 等)</td>
<td>无依赖</td>
<td>app_config.dart、error_mapper.dart</td>
</tr>
<tr>
<td><strong>SDK Packages</strong></td>
<td>可复用技术能力packages/*_sdk</td>
<td>无依赖</td>
<td>NetworkSDK、StorageSDK、L10nSDK</td>
</tr>
<tr>
<td><strong>Core UI Layer</strong></td>
<td>基础定义、基础组件、业务组合组件</td>
<td>→ Core</td>
<td>AppButton、AppDialog、base/colors</td>
</tr>
</tbody>
</table>
<h3>依赖倒置原则DIP</h3>
<p><strong>核心思想</strong>:高层模块不依赖低层模块,两者都依赖抽象(接口)</p>
<div class="mermaid">
flowchart TD
VM[ViewModel<br/>表现层]
UC[UseCase<br/>业务层]
RI[Repository Interface<br/>接口定义]
RImpl[Repository Impl<br/>数据层实现]
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
</div>
<p><strong>示例</strong></p>
<pre><code class="language-dart">// Domain 层定义接口
abstract class ChatRepository {
Future&lt;List&lt;Message&gt;&gt; getMessages(String chatId);
Future&lt;void&gt; sendMessage(Message message);
}
// Data 层实现接口
class ChatRepositoryImpl implements ChatRepository {
final ApiClient _client;
final MessageLocalDataSource _localDataSource;
@override
Future&lt;List&lt;Message&gt;&gt; getMessages(String chatId) async {
// 实现数据获取逻辑
}
}
// Presentation 层使用接口
class ChatViewModel {
final ChatRepository _repository; // 依赖接口,不依赖实现
Future&lt;void&gt; loadMessages() async {
final messages = await _repository.getMessages(chatId);
// ...
}
}
</code></pre>
<h3>Repository 模式</h3>
<p><strong>作用</strong>:将数据访问逻辑封装起来,对上层提供统一的数据访问接口</p>
<p><strong>优势</strong></p>
<ul>
<li>数据源可替换:可以从网络、本地数据库、缓存等任意数据源获取</li>
<li>业务逻辑与数据访问分离:业务层不关心数据来自哪里</li>
<li>易于测试:可以轻松 Mock Repository</li>
</ul>
<div class="mermaid">
flowchart LR
UC[UseCase<br/>业务逻辑]
Repo[Repository<br/>数据仓库]
Remote[Remote Data Source<br/>网络数据源]
Local[Local Data Source<br/>本地数据源]
Cache[Cache<br/>缓存]
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
</div>
<hr>
<h2 id="part4-how" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第四部分怎么做How- 详细实现</h2>
<h2 id="ui-层模块详解">UI 层模块详解</h2>
<h3 id="3-1-ui-层职责">3.1 UI 层职责</h3>
<p>UI 层是应用的最外层,负责:</p>
<ul>
<li>展示用户界面</li>
<li>接收用户交互</li>
<li>调用 ViewModel 方法</li>
<li>监听 ViewModel 状态变化</li>
<li>响应式更新 UI</li>
</ul>
<h3 id="3-1-1-ui-层详细分层">3.1.1 UI 层详细分层结构</h3>
<p><strong>UI 层不是单一层级,而是有明确的分层结构:</strong></p>
<div class="mermaid">
flowchart TD
UI[UI Layer] --> DesignSystem[Design System<br/>设计系统]
UI --> Foundation[Foundation<br/>基础组件]
UI --> Business[Business Components<br/>业务组件]
UI --> Pages[Pages<br/>页面]
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<br/>功能组件]
Pages --> FeaturePages[Feature Pages<br/>功能页面]
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
</div>
<h4>UI 层分层说明</h4>
<table>
<thead>
<tr>
<th>层级</th>
<th>职责</th>
<th>示例</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>L1: Design System</strong><br/>设计系统</td>
<td>定义应用的视觉语言、颜色、字体、间距等设计规范</td>
<td>Colors、Typography、Spacing、BorderRadius</td>
<td>与 Figma 设计稿一一对应</td>
</tr>
<tr>
<td><strong>L2: Foundation</strong><br/>基础组件</td>
<td>可复用的 UI 基础组件,不包含业务逻辑</td>
<td>Button、TextField、Card、Avatar</td>
<td>Atomic Design 原则</td>
</tr>
<tr>
<td><strong>L3: Business Components</strong><br/>业务组件</td>
<td>包含业务逻辑的复用组件</td>
<td>MessageBubble、ChatItem、ContactCard</td>
<td>Feature 级别复用</td>
</tr>
<tr>
<td><strong>L4: Pages</strong><br/>页面</td>
<td>完整的页面,组合各种组件</td>
<td>ChatPage、ChatListPage、ContactPage</td>
<td>Feature 独有</td>
</tr>
</tbody>
</table>
<h4>L1: Design System设计系统</h4>
<p><strong>核心原则</strong>:与 Figma 设计稿完全对应,确保设计与实现一致。</p>
<h5>1.1 Colors颜色</h5>
<pre><code class="language-dart">/// 颜色定义 - 与 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);
}
</code></pre>
<h5>1.2 Typography字体</h5>
<pre><code class="language-dart">/// 字体定义 - 与 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,
);
}
</code></pre>
<h5>1.3 Design Tokens基础定义</h5>
<pre><code class="language-dart">/// 基础定义 - 间距、圆角、阴影等
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;
}
</code></pre>
<h5>1.4 Theme主题 - 黑暗模式)</h5>
<pre><code class="language-dart">/// 主题定义 - 支持亮色/暗色模式
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),
),
);
}
</code></pre>
<h5>1.5 Figma 设计稿对应规范</h5>
<div style="background: #fff3cd; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p><strong>重要原则</strong>:代码中的命名必须与 Figma 设计稿完全对应</p>
<table style="margin-top: 15px;">
<thead>
<tr>
<th>Figma 命名</th>
<th>代码命名</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Primary/Main</code></td>
<td><code>AppColors.primary</code></td>
<td>主色</td>
</tr>
<tr>
<td><code>Button/Primary</code></td>
<td><code>AppButton.primary()</code></td>
<td>主按钮</td>
</tr>
<tr>
<td><code>Text/Headline/Large</code></td>
<td><code>AppTypography.headlineLarge</code></td>
<td>大标题</td>
</tr>
<tr>
<td><code>Spacing/16</code></td>
<td><code>AppTokens.spacing16</code></td>
<td>16pt 间距</td>
</tr>
<tr>
<td><code>Radius/Medium</code></td>
<td><code>AppTokens.radiusMedium</code></td>
<td>中等圆角</td>
</tr>
</tbody>
</table>
<p style="margin-top: 15px;"><strong>好处</strong></p>
<ul style="margin-bottom: 0;">
<li>设计师与开发者使用相同的术语,沟通零障碍</li>
<li>代码审查时可直接对照 Figma 检查实现</li>
<li>设计变更时快速定位需要修改的代码</li>
</ul>
</div>
<h4>L2: Foundation基础组件 - Atomic Design</h4>
<p><strong>遵循 Atomic Design 原则,分为三个层级:</strong></p>
<h5>2.1 Atoms原子组件</h5>
<p>最小的 UI 单元,不可再分。</p>
<pre><code class="language-dart">/// 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),
),
),
);
}
}
</code></pre>
<h5>2.2 Molecules分子组件</h5>
<p>由多个原子组件组合而成。</p>
<pre><code class="language-dart">/// SearchBar - 搜索栏分子组件
class SearchBar extends StatelessWidget {
final String hint;
final ValueChanged&lt;String&gt;? 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(),
),
),
],
),
);
}
}
</code></pre>
<h5>2.3 Organisms有机组件</h5>
<p>由原子和分子组件组合成的复杂组件。</p>
<pre><code class="language-dart">/// 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,
),
),
);
}
}
</code></pre>
<h4>L3: Business Components业务组件</h4>
<p>包含业务逻辑的组件,通常与 Feature 相关。</p>
<pre><code class="language-dart">/// 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,
),
),
),
);
}
}
</code></pre>
<h4>L4: Pages页面</h4>
<p>完整的页面,组合各种组件,连接 ViewModel。</p>
<pre><code class="language-dart">/// 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);
},
),
],
),
);
}
}
</code></pre>
<h4>UI 层目录结构</h4>
<pre><code>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
</code></pre>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<p><strong>核心价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>设计系统</strong>:确保设计与实现一致,与 Figma 完全对应</li>
<li><strong>原子设计</strong>:从小到大构建组件,提升复用性</li>
<li><strong>清晰分层</strong>:基础组件 vs 业务组件,职责明确</li>
<li><strong>主题支持</strong>:亮色/暗色模式统一管理</li>
<li><strong>易于维护</strong>:设计变更只需修改 Design System</li>
</ul>
</div>
<h3 id="3-2-多平台适配">3.2 多平台适配</h3>
<p><strong>核心理念</strong>一套代码适配多个平台iOS、Android、Web、Windows、macOS、Linux通过平台检测和自适应组件实现平台特定的 UI 和交互。</p>
<h4>支持的平台</h4>
<table>
<thead>
<tr>
<th>平台类型</th>
<th>具体平台</th>
<th>设计规范</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>移动端</strong></td>
<td>iOS、Android</td>
<td>Cupertino / Material Design</td>
<td>触摸交互、竖屏优先</td>
</tr>
<tr>
<td><strong>桌面端</strong></td>
<td>Windows、macOS、Linux</td>
<td>Fluent / macOS / GNOME</td>
<td>鼠标键盘、大屏幕</td>
</tr>
<tr>
<td><strong>Web 端</strong></td>
<td>浏览器</td>
<td>响应式设计</td>
<td>跨浏览器兼容</td>
</tr>
</tbody>
</table>
<h4>3.2.1 平台检测与适配策略</h4>
<pre><code class="language-dart">/// 平台工具类 - 统一平台检测
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 }
</code></pre>
<h4>3.2.2 响应式布局</h4>
<p><strong>根据屏幕尺寸自动调整布局:</strong></p>
<pre><code class="language-dart">/// 响应式布局组件
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(),
);
}
}
</code></pre>
<h4>3.2.3 平台自适应组件</h4>
<p><strong>根据平台自动选择 Material 或 Cupertino 风格:</strong></p>
<pre><code class="language-dart">/// 平台自适应按钮
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&lt;Widget&gt;? 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&lt;bool?&gt; showConfirm(
BuildContext context, {
required String title,
required String content,
}) {
// iOS 使用 CupertinoAlertDialog
if (PlatformAdapter.isIOS) {
return showCupertinoDialog&lt;bool&gt;(
context: context,
builder: (context) =&gt; CupertinoAlertDialog(
title: Text(title),
content: Text(content),
actions: [
CupertinoDialogAction(
child: Text('取消'),
onPressed: () =&gt; Navigator.pop(context, false),
),
CupertinoDialogAction(
child: Text('确定'),
isDestructiveAction: true,
onPressed: () =&gt; Navigator.pop(context, true),
),
],
),
);
}
// Android 使用 Material AlertDialog
else {
return showDialog&lt;bool&gt;(
context: context,
builder: (context) =&gt; AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
child: Text('取消'),
onPressed: () =&gt; Navigator.pop(context, false),
),
TextButton(
child: Text('确定'),
onPressed: () =&gt; Navigator.pop(context, true),
),
],
),
);
}
}
}
</code></pre>
<h4>3.2.4 平台特定交互</h4>
<pre><code class="language-dart">/// 平台特定的滑动返回手势
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&lt;ContextMenuItem&gt; 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) {
// 显示移动端底部菜单
}
}
</code></pre>
<h4>3.2.5 屏幕尺寸断点</h4>
<pre><code class="language-dart">/// 屏幕断点定义
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;
}
}
}
</code></pre>
<h4>3.2.6 多平台目录结构</h4>
<pre><code>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 # 桌面布局
</code></pre>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #2196f3; margin: 20px 0;">
<p><strong>多平台适配核心价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>一套代码</strong>:维护成本低,所有平台同步更新</li>
<li><strong>平台原生感</strong>:自动适配平台特定的设计规范和交互</li>
<li><strong>响应式设计</strong>:自动适应不同屏幕尺寸</li>
<li><strong>性能优化</strong>:根据平台特性优化渲染和交互</li>
<li><strong>用户体验一致</strong>:核心功能在所有平台保持一致</li>
</ul>
</div>
<h4>3.2.7 平台适配最佳实践</h4>
<table>
<thead>
<tr>
<th>场景</th>
<th>推荐方案</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>导航栏</strong></td>
<td>PlatformAppBar</td>
<td>iOS 用 CupertinoNavigationBarAndroid 用 AppBar</td>
</tr>
<tr>
<td><strong>对话框</strong></td>
<td>PlatformDialog</td>
<td>iOS 用 CupertinoAlertDialogAndroid 用 AlertDialog</td>
</tr>
<tr>
<td><strong>按钮</strong></td>
<td>PlatformButton</td>
<td>iOS 用 CupertinoButtonAndroid 用 ElevatedButton</td>
</tr>
<tr>
<td><strong>滑动返回</strong></td>
<td>自动检测平台</td>
<td>iOS 支持侧滑返回Android 使用返回按钮</td>
</tr>
<tr>
<td><strong>右键菜单</strong></td>
<td>桌面端显示,移动端长按</td>
<td>根据平台调整交互方式</td>
</tr>
<tr>
<td><strong>布局</strong></td>
<td>ResponsiveLayout</td>
<td>手机单栏、平板双栏、桌面三栏</td>
</tr>
</tbody>
</table>
<h3 id="3-3-feature-ui-组织">3.3 Feature UI 组织</h3>
<p><strong>核心理念</strong>UI 层按 Feature 组织,每个页面的 UI 组件都在其对应的 Feature 目录下。</p>
<div class="mermaid">
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/<br/>chat_page.dart]
Chat --> ChatWidgets[features/chat/view/widgets/<br/>message_bubble.dart<br/>message_input_bar.dart]
ChatList --> ChatListPage[features/chat_list/view/<br/>chat_list_page.dart]
ChatList --> ChatListWidgets[features/chat_list/view/widgets/<br/>chat_list_item.dart]
Contact --> ContactPage[features/contact/view/<br/>contact_page.dart]
Contact --> ContactWidgets[features/contact/view/widgets/<br/>contact_item.dart]
Search --> SearchPage[features/search/view/<br/>search_page.dart]
Call --> CallPage[features/call/view/<br/>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
</div>
<h3 id="3-4-ui-层目录结构">3.4 UI 层目录结构</h3>
<pre><code>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 # 通话控制按钮
</code></pre>
<h3 id="3-4-主要-feature-页面">3.4 主要 Feature 页面</h3>
<h4>Chat Feature - 聊天功能</h4>
<ul>
<li><strong>位置</strong><code>features/chat/view/chat_page.dart</code></li>
<li><strong>职责</strong>:消息列表展示、消息发送、多媒体消息、消息状态显示</li>
<li><strong>专用组件</strong>MessageBubble、InputBar、MessageList</li>
</ul>
<h4>Chat List Feature - 会话列表功能</h4>
<ul>
<li><strong>位置</strong><code>features/chat_list/view/chat_list_page.dart</code></li>
<li><strong>职责</strong>:显示所有会话、未读消息提示、会话操作(删除/置顶)</li>
<li><strong>专用组件</strong>ChatItem、SwipeActions</li>
</ul>
<h4>Contact Feature - 联系人功能</h4>
<ul>
<li><strong>位置</strong><code>features/contact/view/contact_page.dart</code></li>
<li><strong>职责</strong>:联系人列表、分组展示、联系人搜索、联系人详情</li>
<li><strong>专用组件</strong>ContactItem、SectionHeader</li>
</ul>
<h4>Search Feature - 搜索功能</h4>
<ul>
<li><strong>位置</strong><code>features/search/view/search_page.dart</code></li>
<li><strong>职责</strong>:全局搜索、消息搜索、联系人搜索、搜索历史</li>
<li><strong>专用组件</strong>SearchResultItem</li>
</ul>
<h4>Call Feature - 通话功能</h4>
<ul>
<li><strong>位置</strong><code>features/call/view/call_page.dart</code></li>
<li><strong>职责</strong>:语音通话、视频通话、通话控制、通话状态</li>
<li><strong>专用组件</strong>CallControls</li>
</ul>
<blockquote>
<p><strong>设计原则</strong>UI 层的每个页面都在其对应的 Feature 目录下,与该 Feature 的 Presentation 层和 Domain 层垂直对齐,形成高内聚的功能模块。</p>
</blockquote>
<hr>
<hr>
<h2 id="路由系统">路由系统go_router</h2>
<h3 id="路由-是什么">路由是什么</h3>
<p>路由就是「页面地址 → 页面」的映射表。打开 App 时系统根据当前地址决定显示哪个页面,点击按钮时通过地址跳转到另一个页面。</p>
<h3 id="shell-是什么">Shell 是什么</h3>
<p><strong>Shell</strong>(壳层)是一个持久存在的 UI 框架,内容区域在里面切换,而框架本身不销毁。类比一下:</p>
<ul>
<li>微信底部有「微信 / 通讯录 / 发现 / 我」四个 Tab这四个 Tab 的底部导航栏始终可见,这就是 Shell</li>
<li>点击不同 Tab底部导航栏不动只有上方内容在切换</li>
<li>但打开「设置 → 关于微信」这类页面时,底部导航栏消失了,这是跳出了 Shell</li>
</ul>
<p>在本项目里:</p>
<ul>
<li><strong>Shell 内(带导航栏)</strong><code>/chat</code><code>/contact</code><code>/settings</code>——底部导航栏始终可见</li>
<li><strong>Shell 外(全屏)</strong><code>/chat/detail</code><code>/chat/:id</code><code>/settings/theme</code><code>/login</code>——全屏独占,没有底部导航栏</li>
</ul>
<p><code>AppTab</code> 就是 Shell 组件,它只负责渲染底部导航栏和容纳当前页面内容,自身不包含任何业务逻辑。</p>
<h3 id="为什么禁用-navigator-push">为什么禁止使用 Navigator.push</h3>
<p>传统写法:</p>
<pre><code class="language-dart">// 禁止 ❌
Navigator.push(context, MaterialPageRoute(builder: (_) =&gt; const ThemeView()));
</code></pre>
<p>禁止原因:</p>
<ul>
<li><strong>绕过了守卫</strong>:直接 <code>Navigator.push</code> 不经过 go_router 的 <code>redirect</code>,未登录用户可以直接跳进受保护页面</li>
<li><strong>路径分散</strong>:目标页面的引用散落在各处 <code>onTap</code>,重构时要全局搜索替换</li>
<li><strong>破坏 Shell</strong>:在 go_router 管理的路由中混用 <code>Navigator.push</code>,可能导致底部导航栏消失或 Tab 状态丢失</li>
<li><strong>深链接失效</strong>go_router 无法感知通过 <code>Navigator.push</code> 打开的页面,通知点击跳转等场景会出问题</li>
</ul>
<p>正确写法:</p>
<pre><code class="language-dart">// 正确 ✅
context.push(AppRouteName.settingsTheme.path); // 压栈,可以返回
context.go(AppRouteName.chat.path); // 替换历史,不可返回
</code></pre>
<p><strong>go_router</strong> 集中解决上述问题:统一声明路由、统一拦截、路径字符串集中在 <code>AppRouteName</code> 枚举管理。</p>
<h3 id="路由文件结构">文件结构</h3>
<pre><code>app/router/
├── app_router.dart # 路由表 + routerProvider核心
├── app_route_name.dart # AppRouteName 枚举,路径常量 + fromPath()
└── guards/
└── auth_guard.dart # 登录守卫(拦截未登录访问)
</code></pre>
<h3 id="路由路径常量">路径常量app_route_name.dart</h3>
<p>所有路径字符串只在这一个文件里写,用枚举定义。其他地方引用 <code>AppRouteName.xxx.path</code>,不允许硬编码字符串。</p>
<p>用枚举而不是常量类,是因为守卫里的 <code>switch</code> 需要穷举枚举值:新加路由时,若守卫的 <code>switch</code> 没有补对应的 <code>case</code>,编译器直接报错,防止漏掉权限判断。</p>
<pre><code class="language-dart">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';
}
</code></pre>
<blockquote>
<p><strong>规则</strong>:任何地方都不允许硬编码路径字符串。<code>context.push / context.go</code><code>.path</code>;含路径参数的路由用对应的静态方法(如 <code>chatDetailByIdPath</code>);路径字符串只在枚举里写一次。</p>
</blockquote>
<h3 id="路由表结构">路由表结构app_router.dart</h3>
<p>整个路由表分两类:</p>
<table>
<tr><th>类型</th><th>路由</th><th>TabBar</th></tr>
<tr><td>Shell 内StatefulShellRoute branches</td><td>/chat、/contact、/settings</td><td>始终可见</td></tr>
<tr><td>Shell 外parentNavigatorKey = _rootKey</td><td>/chat/detail、/chat/:id、/settings/theme、/login</td><td>隐藏</td></tr>
</table>
<pre><code class="language-dart">// Root Navigator Key全屏路由声明 parentNavigatorKey 时引用,
// 确保 push 时覆盖整个 ShellTabBar 消失)
final _rootKey = GlobalKey&lt;NavigatorState&gt;();
final routerProvider = Provider&lt;GoRouter&gt;((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) =&gt; authGuard(authNotifier, state),
routes: [
// ── Shell 内:底部导航栏始终可见 ─────────────────────────────────
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return AppTab(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.chat.path, builder: (_, __) =&gt; const ChatPage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.contact.path, builder: (_, __) =&gt; const ContactPage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: AppRouteName.settings.path, builder: (_, __) =&gt; const SettingsPage()),
]),
],
),
// ── Shell 外:全屏页面,无底部导航栏 ─────────────────────────────
// parentNavigatorKey: _rootKey 确保路由覆盖 ShellTabBar 消失
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: (_, __) =&gt; const ThemeView(),
),
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.login.path,
builder: (_, __) =&gt; const LoginPage(),
),
],
);
});
</code></pre>
<h3 id="shell-route-是什么">StatefulShellRoute 是什么</h3>
<p><code>StatefulShellRoute.indexedStack</code> 负责维护底部 Tab 导航,每个 <code>branches</code> 对应一个 Tab 分支。它和普通 <code>IndexedStack</code> 的区别:</p>
<table>
<tr><th></th><th>IndexedStack旧方式</th><th>StatefulShellRoutego_router</th></tr>
<tr><td>Tab 状态保持</td><td>✅ 是</td><td>✅ 是</td></tr>
<tr><td>路径支持</td><td>❌ 无路径概念</td><td>✅ 每个 Tab 都有真实 URL 路径</td></tr>
<tr><td>深链接</td><td>❌ 不支持</td><td>✅ 支持</td></tr>
<tr><td>Tab 内子页面</td><td>❌ 需手动处理</td><td>✅ 独立 Navigator 栈</td></tr>
<tr><td>守卫</td><td>❌ 无统一拦截</td><td>✅ 全局 redirect</td></tr>
</table>
<p><code>branches</code> 是并列关系,不是层级关系。三个 Tab 分支互相独立,各自维护自己的导航栈:</p>
<pre><code>StatefulShellRouteShell 内TabBar 可见)
branches[0] → /chat ← 聊天 Tab独立栈
branches[1] → /contact ← 联系人 Tab独立栈
branches[2] → /settings ← 设置 Tab独立栈
Root NavigatorShell 外TabBar 隐藏parentNavigatorKey: _rootKey
/chat/detail ← extra 传参详情页
/chat/:id ← 路径参数详情页
/settings/theme ← 主题设置页
/login ← 登录页
</code></pre>
<h3 id="如何在页面间跳转">如何在页面间跳转</h3>
<p><code>context</code> 调用,不需要 <code>ref</code></p>
<table>
<tr><th>场景</th><th>方法</th><th>说明</th></tr>
<tr><td>进入子页面可返回TabBar 隐藏)</td><td><code>context.push(AppRouteName.xxx.path)</code></td><td>压到 Root Navigator覆盖 Shell有返回按钮</td></tr>
<tr><td>带参数进入子页面extra</td><td><code>context.push(AppRouteName.xxx.path, extra: obj)</code></td><td>extra 传 Dart 对象;路由 builder 解包后以构造参数注入目标页</td></tr>
<tr><td>带参数进入子页面(路径参数)</td><td><code>context.push(AppRouteName.xxxByIdPath(id))</code></td><td>id 内嵌在 URL 中;适合深链接 / 推送通知跳转</td></tr>
<tr><td>切换 TabTabBar 可见)</td><td><code>context.go(AppRouteName.xxx.path)</code></td><td>替换整个历史Tab 高亮切换,不可返回</td></tr>
<tr><td>登录成功跳首页</td><td><code>context.go(AppRouteName.chat.path)</code></td><td>替换历史,防止返回到登录页</td></tr>
<tr><td>返回上一页</td><td><code>context.pop()</code></td><td>弹栈</td></tr>
<tr><td>返回并传值给上层</td><td><code>context.pop(result)</code></td><td>上层用 <code>await context.push(...)</code> 接收</td></tr>
<tr><td>弹窗 / Alert</td><td><code>showDialog(...)</code></td><td>Flutter 原生go_router 不管理,直接用</td></tr>
<tr><td>底部弹层</td><td><code>showModalBottomSheet(...)</code></td><td>Flutter 原生go_router 不管理,直接用</td></tr>
</table>
<pre><code class="language-dart">// 进入子页面TabBar 隐藏,可返回)
context.push(AppRouteName.settingsTheme.path);
// 带参数进入子页面extra 传 Dart Record
context.push(
AppRouteName.chatDetail.path,
extra: (conversationId: '42', title: '技术支持'),
);
// 带参数进入子页面:路径参数
context.push(AppRouteName.chatDetailByIdPath('99'));
// 切换 TabTabBar 可见,不可返回)
context.go(AppRouteName.contact.path);
// 返回
context.pop();
// 返回并传值
final result = await context.push&lt;String&gt;(AppRouteName.settingsTheme.path);
// 弹窗Flutter 原生)
showDialog(context: context, builder: (_) =&gt; const AlertDialog(...));
</code></pre>
<blockquote>
<p><strong>禁止</strong>使用 <code>Navigator.push(context, MaterialPageRoute(...))</code>:不经过 go_router redirect守卫失效路径散落各处破坏 Tab 状态;深链接 / 通知跳转失效。</p>
</blockquote>
<h3 id="带参数路由">带参数路由</h3>
<p>go_router 有两种传参方式,根据场景选择:</p>
<table>
<tr><th></th><th>extra传对象</th><th>路径参数(:id</th></tr>
<tr><td>适用场景</td><td>列表点入详情,导航时已有完整数据</td><td>推送通知深链接,只携带一个 ID</td></tr>
<tr><td>URL 可见</td><td></td><td></td></tr>
<tr><td>数据来源</td><td>直接传对象,不需要额外请求</td><td>按 ID 从 Repository 重新拉取</td></tr>
<tr><td>代码量</td><td></td><td>多(需 .family provider</td></tr>
</table>
<p>日常开发优先用 <strong>extra</strong>;接入推送通知后,需要从通知冷启动进入会话时,再补路径参数版本。</p>
<h4>extra 传参完整示例</h4>
<p><strong>Step 1枚举声明路由</strong>(静态路径,注释写明 extra 类型)</p>
<pre><code class="language-dart">// app_route_name.dart
// Chat 子路由
// extra: ({String conversationId, String title})
chatDetail('/chat/detail'),
</code></pre>
<p><strong>Step 2路由表注册builder 负责解包 extra</strong></p>
<pre><code class="language-dart">// app_router.dart
// Shell 外顶层路由parentNavigatorKey: _rootKey 覆盖整个 ShellTabBar 隐藏
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,
);
},
),
</code></pre>
<p><strong>Step 3目标页面只接受构造参数不感知 GoRouter</strong></p>
<pre><code class="language-dart">// chat_detail_page.dart
class ChatDetailPage extends StatelessWidget {
const ChatDetailPage({
super.key,
required this.conversationId,
required this.title,
});
final String conversationId;
final String title;
// ...
}
</code></pre>
<p><strong>Step 4导航时附带 extra</strong></p>
<pre><code class="language-dart">context.push(
AppRouteName.chatDetail.path,
extra: (conversationId: conversation.id, title: conversation.title),
);
</code></pre>
<p>extra 使用 Dart 3 匿名 Record轻量且类型安全无需定义额外 class。</p>
<h3 id="tab-如何切换">AppTabTab 如何切换</h3>
<p><code>AppTab</code> 是底部导航栏的持久容器,负责渲染底部 Tab 栏和持有当前 Tab 的内容区域。它自身不管理任何状态Tab 的当前索引由 go_router 传入的 <code>StatefulNavigationShell</code> 维护:</p>
<pre><code class="language-dart">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) =&gt; navigationShell.goBranch(
index,
// 再次点击已激活的 Tab 时回到该 Tab 的首页
initialLocation: index == navigationShell.currentIndex,
),
items: const [...],
),
);
}
}
</code></pre>
<p>上面的 <code>goBranch</code> 只用在 AppTab 内部的 <code>onTap</code>,处理「重复点同一 Tab 时回到该 Tab 首页」的导航栏专属逻辑。<strong>业务页面里不调 <code>goBranch</code></strong></p>
<h4>从业务页面切换 Tab</h4>
<p>在任意页面主动切换 Tab<code>context.go</code></p>
<pre><code class="language-dart">// 切换到联系人 Tab
context.go(AppRouteName.contact.path);
// 切换到设置 Tab
context.go(AppRouteName.settings.path);
</code></pre>
<table>
<tr><th></th><th>context.go</th><th>context.push</th></tr>
<tr><td>历史栈</td><td>替换(不可返回)</td><td>压栈(可返回)</td></tr>
<tr><td>返回按钮</td><td>不显示</td><td>自动显示</td></tr>
<tr><td>适用场景</td><td>切换 Tab、登录后跳首页</td><td>进入子页面</td></tr>
</table>
<h4>Shell 外路由的路径前缀不影响 TabBar</h4>
<p>Shell 外路由(声明了 <code>parentNavigatorKey: _rootKey</code>)放到 Root Navigator 上,会覆盖整个 Shell<strong>TabBar 始终隐藏</strong>,与路径前缀无关。</p>
<p>例如 <code>/settings/theme</code> 虽然前缀是 <code>/settings</code>,但它是 Shell 外路由,导航后 TabBar 不可见:</p>
<pre><code class="language-dart">// 全屏打开主题页TabBar 隐藏,可返回
context.push(AppRouteName.settingsTheme.path);
</code></pre>
<p>如果需要"在某个 Tab 内打开子页面、保留 TabBar",要把子路由注册为 Shell 内路由(放进对应 <code>StatefulShellBranch</code><code>routes</code>,不加 <code>parentNavigatorKey</code>)。</p>
<h3 id="登录守卫">登录守卫auth_guard.dart</h3>
<p>守卫是 go_router 的全局拦截器,每次跳转路由前都会执行。返回 <code>null</code> 表示放行,返回路径字符串表示重定向到那个路径。</p>
<pre><code class="language-dart">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;
}
}
</code></pre>
<p><code>switch(route)</code> 而不是 <code>if-else</code> 的原因Dart 的枚举 <code>switch</code> 是穷举的。在 <code>AppRouteName</code> 里新增一个枚举值(即新加路由),如果忘了在守卫的 <code>switch</code> 里补对应的 <code>case</code>,编译器会直接报错,强制你决定这条路由的权限。</p>
<h3 id="refreshListenable-机制">refreshListenable 机制</h3>
<p>守卫只在「路由跳转」时执行一次。但当用户「退出登录」后,页面没有跳转,守卫不会自动重新执行。</p>
<p><code>refreshListenable</code> 解决这个问题:把 <code>AuthNotifier</code>(它继承自 <code>ChangeNotifier</code>)传给 go_router每当 <code>AuthNotifier.notifyListeners()</code> 被调用时go_router 自动重新执行 redirect</p>
<pre><code class="language-dart">GoRouter(
refreshListenable: authNotifier, // 监听 AuthNotifier
redirect: (context, state) =&gt; authGuard(authNotifier, state),
...
)
// 当用户点击「退出登录」:
ref.read(authNotifierProvider).logout();
// ↓ AuthNotifier.logout() 调用 notifyListeners()
// ↓ go_router 收到通知,重新执行 authGuard
// ↓ authGuard 发现未登录,返回 AppRouteName.login.path
// ↓ 自动跳转到登录页
</code></pre>
<h3 id="如何添加新路由">如何添加一个新路由</h3>
<p>以添加「个人资料页 /profile」为例共三步</p>
<p><strong>Step 1在 AppRouteName 枚举追加新值</strong></p>
<pre><code class="language-dart">// app_route_name.dart
enum AppRouteName {
chat('/chat'),
contact('/contact'),
settings('/settings'),
settingsTheme('/settings/theme'),
login('/login'),
profile('/profile'); // 新增
...
}
</code></pre>
<p>加完之后,守卫的 <code>switch</code> 会立即编译报错,提示你补上 <code>case AppRouteName.profile:</code>,决定这条路由是否需要登录。</p>
<p><strong>Step 2在守卫 switch 补 case</strong></p>
<pre><code class="language-dart">// 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;
}
</code></pre>
<p><strong>Step 3在 app_router.dart 注册路由</strong></p>
<p>全屏页面(无底部导航栏):加到顶层 <code>routes</code> 列表,加上 <code>parentNavigatorKey: _rootKey</code>,确保覆盖整个 Shell、TabBar 隐藏。</p>
<pre><code class="language-dart">// app_router.dartShell 外,全屏)
GoRoute(
parentNavigatorKey: _rootKey,
path: AppRouteName.profile.path,
builder: (_, __) =&gt; const ProfilePage(),
),
</code></pre>
<p>Tab 内子页面(保留 TabBar加到对应 <code>StatefulShellBranch</code><code>routes</code> 里,<strong>不加</strong> <code>parentNavigatorKey</code>,路由放到 Branch NavigatorTabBar 保持可见。</p>
<pre><code class="language-dart">// app_router.dartShell 内TabBar 可见)
StatefulShellBranch(
routes: [
GoRoute(
path: AppRouteName.settings.path,
builder: (_, __) =&gt; const SettingsPage(),
routes: [
// 此处不加 parentNavigatorKey路由在 Branch Navigator 内
GoRoute(path: AppRouteName.profile.segment, builder: (_, __) =&gt; const ProfilePage()),
],
),
],
),
</code></pre>
<p><strong>Step 4在需要的地方跳转</strong></p>
<pre><code class="language-dart">onTap: () =&gt; context.push(AppRouteName.profile.path),
</code></pre>
<h3 id="路由守卫接入正式-token">接入正式 tokenstorage_sdk 就绪后)</h3>
<p>当前守卫用 <code>AuthNotifier._isLoggedIn</code>(内存变量,重启后重置)做 Demo。storage_sdk 接入后只需修改 <code>AuthNotifier</code>,守卫本身无需改动:</p>
<pre><code class="language-dart">class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
// 改为从安全存储读取:
Future&lt;void&gt; initialize() async {
final token = await secureStorage.read('token');
_isLoggedIn = token != null &amp;&amp; token.isNotEmpty;
notifyListeners();
}
Future&lt;void&gt; login(String token) async {
await secureStorage.write('token', token);
_isLoggedIn = true;
notifyListeners();
}
Future&lt;void&gt; logout() async {
await secureStorage.delete('token');
_isLoggedIn = false;
notifyListeners();
}
}
</code></pre>
<hr>
<hr>
<h2 id="presentation-层模块详解">Presentation 层模块详解</h2>
<h3 id="4-1-presentation-层职责">4.1 Presentation 层职责</h3>
<p>Presentation 层实现 MVVM 模式中的 ViewModel负责</p>
<ul>
<li>管理 UI 状态</li>
<li>处理用户交互逻辑</li>
<li>调用 Repository复杂多步编排场景可提取 UseCase</li>
<li>数据格式转换Entity → UI Model</li>
<li>通知 UI 更新</li>
</ul>
<h3 id="4-2-feature-presentation-组织">4.2 Feature Presentation 组织</h3>
<p><strong>核心理念</strong>:每个 Feature 的 ViewModel 都在其对应的 Feature 目录下的 presentation/ 子目录中。</p>
<div class="mermaid">
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/<br/>chat_view_model.dart + chat_state.dart]
ChatListPres --> ChatListVM[features/chat_list/presentation/<br/>chat_list_view_model.dart]
ContactPres --> ContactVM[features/contact/presentation/<br/>contact_view_model.dart]
SearchPres --> SearchVM[features/search/presentation/<br/>search_view_model.dart]
CallPres --> CallVM[features/call/presentation/<br/>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
</div>
<h3 id="4-3-presentation-层目录结构">4.3 Presentation 层目录结构</h3>
<pre><code>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
</code></pre>
<h3 id="4-4-viewmodel-设计">4.4 ViewModel 设计</h3>
<div class="mermaid">
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
</div>
<h4>Riverpod ViewModel 实现方式</h4>
<p>使用 Riverpod有两种主要的 ViewModel 实现方式:</p>
<h5>方式一标准方式StateNotifier + 手动定义)</h5>
<pre><code>// 1. 定义 State 类(使用 freezed
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List&lt;Message&gt; messages,
@Default(false) bool isLoading,
@Default('') String error,
Message? selectedMessage,
}) = _ChatState;
}
// 2. 定义 ViewModel — 直接调用 Repository
class ChatViewModel extends StateNotifier&lt;ChatState&gt; {
ChatViewModel(this._chatRepository) : super(const ChatState());
final ChatRepository _chatRepository;
// 发送消息
Future&lt;void&gt; 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&lt;void&gt; 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&lt;ChatViewModel, ChatState&gt;((ref) {
return ChatViewModel(ref.watch(chatRepositoryProvider));
});
</code></pre>
<h5>方式二现代方式Notifier + 代码生成)⭐ 推荐</h5>
<pre><code>// 使用 riverpod_generator 和 freezed
part 'chat_view_model.g.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List&lt;Message&gt; messages,
@Default(false) bool isLoading,
@Default('') String error,
}) = _ChatState;
}
@riverpod
class ChatViewModel extends _$ChatViewModel {
@override
ChatState build() =&gt; const ChatState();
// ViewModel 直接调用 Repository
Future&lt;void&gt; 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&lt;void&gt; 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);
}
}
}
</code></pre>
<h4>UI 层使用 ViewModel</h4>
<pre><code>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) =&gt; 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) =&gt; viewModel.sendMessage(content),
),
],
),
);
}
}
</code></pre>
<blockquote>
<p><strong>为什么不需要 Event / Effect 文件?</strong></p>
<p>在 BLoC 模式中Event 用于触发状态变更Effect 用于处理副作用导航、Toast</p>
<p>Riverpod 中这两个概念有更自然的替代方案:</p>
<p><strong>Event → 直接方法调用</strong><code>viewModel.sendMessage(content)</code>,无需中间层</p>
<p><strong>Effect → <code>ref.listen()</code></strong>:监听状态字段变化,在 View 层触发导航/弹窗</p>
</blockquote>
<h3 id="4-5-主要-viewmodel">4.5 主要 ViewModel</h3>
<h4>ChatViewModel</h4>
<ul>
<li><strong>位置</strong><code>features/chat/presentation/chat_view_model.dart</code></li>
<li><strong>职责</strong>:消息列表状态管理、发送消息逻辑、消息加载分页、消息状态更新</li>
<li><strong>调用</strong>ChatRepository直接调用</li>
</ul>
<h4>ChatListViewModel</h4>
<ul>
<li><strong>位置</strong><code>features/chat_list/presentation/chat_list_view_model.dart</code></li>
<li><strong>职责</strong>:会话列表状态、未读数管理、会话操作</li>
<li><strong>调用</strong>ChatRepository直接调用</li>
</ul>
<h4>ContactViewModel</h4>
<ul>
<li><strong>位置</strong><code>features/contact/presentation/contact_view_model.dart</code></li>
<li><strong>职责</strong>:联系人列表状态、联系人搜索、联系人操作</li>
<li><strong>调用</strong>ContactRepository直接调用</li>
</ul>
<h4>SearchViewModel</h4>
<ul>
<li><strong>位置</strong><code>features/search/presentation/search_view_model.dart</code></li>
<li><strong>职责</strong>:全局搜索状态管理、搜索历史管理</li>
<li><strong>调用</strong>:多个 Repository跨模块搜索</li>
</ul>
<h4>CallViewModel</h4>
<ul>
<li><strong>位置</strong><code>features/call/presentation/call_view_model.dart</code></li>
<li><strong>职责</strong>:通话状态管理、通话控制逻辑</li>
<li><strong>调用</strong>CallRepository直接调用</li>
</ul>
<blockquote>
<p><strong>设计原则</strong>:每个 Feature 的 ViewModel 独立管理该 Feature 的状态,直接调用 Repository 执行数据操作。当业务逻辑复杂(多步编排、跨模块协调)时,可提取 UseCase 封装。</p>
</blockquote>
<hr>
<hr>
<h2 id="domain-层模块详解">Domain 层模块详解</h2>
<h3 id="5-1-domain-层职责">5.1 Domain 层职责</h3>
<p>Domain 层是整洁架构的核心,分为两部分:</p>
<h4>Feature 专属 Domainfeatures/*/domain/</h4>
<ul>
<li><strong>Use Cases</strong>:封装该 Feature 的业务逻辑</li>
<li><strong>Entities</strong>:该 Feature 特有的Domain 实体</li>
</ul>
<h4>全局共享 Domaindomain/</h4>
<ul>
<li><strong>Repository 接口</strong>:定义数据访问接口</li>
<li><strong>Value Objects</strong>:跨 Feature 的值对象</li>
</ul>
<h3 id="5-2-domain-层架构">5.2 Domain 层架构</h3>
<div class="mermaid">
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/<br/>Repository 接口定义]
ValueObjects[domain/value_objects/<br/>共享值对象]
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
</div>
<h3 id="5-3-feature-domain-目录结构">5.3 Feature Domain 目录结构</h3>
<pre><code>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 # 结束通话
</code></pre>
<h3 id="5-4-全局-domain-目录结构">5.4 全局 Domain 目录结构</h3>
<pre><code>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 值对象
</code></pre>
<h3 id="5-5-use-case-设计">5.5 Use Case 设计(可选层)</h3>
<blockquote style="border-left: 4px solid #ff9800; background: #fff8e1; padding: 12px 16px;">
<p><strong>⚠️ UseCase 是可选的</strong></p>
<p>大多数 Feature 的 ViewModel 可以<strong>直接调用 Repository</strong>,无需 UseCase 中间层。</p>
<p>只在以下场景提取 UseCase</p>
<p><strong>多步业务编排</strong>:如登录后需写 Token + 更新用户信息 + 上报设备</p>
<p><strong>跨模块协调</strong>:一个操作需要调用多个 Repository</p>
<p><strong>复杂业务规则</strong>:格式校验、权限判断、重试策略等</p>
<p><strong>多 ViewModel 复用</strong>:同一业务逻辑被多个页面调用</p>
<p>典型案例:<code>LoginUseCase</code>(登录 = 调接口 + 写 Token + 转实体,属于多步编排)</p>
</blockquote>
<p>当确实需要 UseCase 时,遵循单一职责原则:</p>
<div class="mermaid">
flowchart TD
VM[ViewModel] -->|大多数场景| Repo[Repository Interface<br/>domain/repositories/]
VM -.->|复杂场景| UC[UseCase<br/>features/*/usecases/]
UC -->|通过接口| Repo
Repo -.实现.-> RepoImpl[Repository Impl<br/>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
</div>
<h3 id="5-6-主要-use-cases">5.6 主要 Use Cases</h3>
<h4>Chat Feature Use Cases</h4>
<ul>
<li><strong>位置</strong><code>features/chat/usecases/</code></li>
<li><code>SendMessageUseCase</code>:发送消息</li>
<li><code>LoadMessagesUseCase</code>:加载消息</li>
<li><code>DeleteMessageUseCase</code>:删除消息</li>
</ul>
<h4>Chat List Feature Use Cases</h4>
<ul>
<li><strong>位置</strong><code>features/chat_list/usecases/</code></li>
<li><code>LoadChatListUseCase</code>:加载会话列表</li>
<li><code>UpdateChatUseCase</code>:更新会话</li>
<li><code>DeleteChatUseCase</code>:删除会话</li>
</ul>
<h4>Contact Feature Use Cases</h4>
<ul>
<li><strong>位置</strong><code>features/contact/usecases/</code></li>
<li><code>LoadContactsUseCase</code>:加载联系人</li>
<li><code>SearchContactUseCase</code>:搜索联系人</li>
</ul>
<h4>Call Feature Use Cases</h4>
<ul>
<li><strong>位置</strong><code>features/call/usecases/</code></li>
<li><code>InitiateCallUseCase</code>:发起通话</li>
<li><code>AnswerCallUseCase</code>:接听通话</li>
<li><code>EndCallUseCase</code>:结束通话</li>
</ul>
<h3 id="5-7-repository-接口">5.7 Repository 接口</h3>
<p><strong>位置</strong><code>domain/repositories/</code>(全局目录)</p>
<p>Domain 层只定义接口,不包含实现:</p>
<ul>
<li><code>MessageRepository</code>:消息数据访问接口</li>
<li><code>ChatRepository</code>:会话数据访问接口</li>
<li><code>ContactRepository</code>:联系人数据访问接口</li>
<li><code>CallRepository</code>:通话数据访问接口</li>
</ul>
<blockquote>
<p><strong>关键原则</strong></p>
<p>1. <strong>UseCase 按需创建</strong>:只在多步编排、跨模块协调等复杂场景使用,简单 CRUD 直接调 Repository</p>
<p>2. <strong>UseCase 在 Feature 目录</strong>:需要时,放在 features/*/usecases/ 下</p>
<p>3. <strong>Repository 接口在全局 domain/</strong>:所有 Repository 接口定义在 domain/repositories/ 下</p>
<p>4. <strong>依赖倒置</strong>UseCase / ViewModel 依赖 Repository 接口,不依赖具体实现</p>
</blockquote>
<hr>
<hr>
<h2 id="data-层模块详解">Data 层模块详解</h2>
<h3 id="6-1-data-层职责">6.1 Data 层职责</h3>
<p>Data 层是全局目录,负责:</p>
<ul>
<li>实现 Domain 层(全局 domain/)定义的 Repository 接口</li>
<li>协调本地和远程数据源</li>
<li>数据缓存策略</li>
<li>数据格式转换DTO ↔ Entity</li>
</ul>
<h3 id="6-2-data-层目录结构">6.2 Data 层目录结构</h3>
<pre><code>lib/data/
├── repositories/ # Repository 实现
│ ├── message_repository_impl.dart # 实现 MessageRepository 接口
│ ├── chat_repository_impl.dart # 实现 ChatRepository 接口
│ ├── contact_repository_impl.dart # 实现 ContactRepository 接口
│ ├── user_repository_impl.dart # 实现 UserRepository 接口
│ └── call_repository_impl.dart # 实现 CallRepository 接口
├── local/ # 本地数据源
│ ├── message_local_ds.dart # 消息本地数据源
│ ├── chat_local_ds.dart # 会话本地数据源
│ ├── contact_local_ds.dart # 联系人本地数据源
│ ├── user_local_ds.dart # 用户本地数据源
│ │
│ ├── drift/ # Drift 数据库
│ │ ├── app_database.dart # Drift 数据库定义
│ │ ├── app_database.g.dart # Drift 生成代码
│ │ # database_connection.dart 已迁移至 storage_sdk数据库连接与 Isolate 生命周期由 SDK 层统一管理)
│ │ ├── tables/ # 表定义
│ │ │ ├── message_table.dart # 消息表
│ │ │ ├── conversation_table.dart # 会话表
│ │ │ └── user_table.dart # 用户表
│ │ ├── daos/ # 数据访问对象
│ │ │ ├── message_dao.dart # 消息 DAO
│ │ │ ├── conversation_dao.dart # 会话 DAO
│ │ │ └── user_dao.dart # 用户 DAO
│ │ ├── migrations/ # 数据库迁移
│ │ │ ├── migration_v1.dart # V1 迁移脚本
│ │ │ └── migration_runner.dart # 迁移执行器
│ │ └── mappers/ # DB ↔ DTO 映射
│ │ ├── message_mapper.dart # 消息映射
│ │ └── conversation_mapper.dart # 会话映射
│ │
│ └── storage/ # 其他本地存储
│ ├── preference_storage.dart # SharedPreferences 封装
│ ├── file_storage.dart # 文件存储管理
│ └── image_cache.dart # 图片缓存
├── remote/ # Request 文件一个端点一个文件Repository 直接调 ApiClient
│ ├── login_request.dart # 登录端点
│ ├── logout_request.dart # 登出端点
│ ├── send_message_request.dart # 发消息端点
│ └── ... # 其他端点 Request 文件
├── cache/ # 缓存
│ ├── cache_manager.dart # 缓存管理器
│ └── cache_policies.dart # 缓存策略
└── models/ # DTO统一归口local / remote 共用)
├── message_dto.dart # 消息 DTO
├── conversation_dto.dart # 会话 DTO
├── user_dto.dart # 用户 DTO
├── contact_dto.dart # 联系人 DTO
└── call_dto.dart # 通话 DTO
</code></pre>
<h3 id="6-3-repository-实现">6.3 Repository 实现</h3>
<div class="mermaid">
flowchart TD
Domain[Domain Layer<br/>domain/repositories/<br/>Repository 接口] -.实现.-> Repo[Data Layer<br/>data/repositories/<br/>Repository 实现]
Repo -->|读取| LocalDS[Local DataSource<br/>data/local/]
Repo -->|请求| ApiClient[ApiClient<br/>networks_sdk]
Repo -->|缓存| Cache[Cache Manager<br/>data/cache/]
LocalDS -->|Drift| DB[(Database)]
ApiClient -->|HTTP/WebSocket| API[API Server]
Cache -->|内存| Memory[Memory Cache]
style Domain fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style Repo fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
style LocalDS fill:#c8e6c9,stroke:#388e3c
style ApiClient fill:#c8e6c9,stroke:#388e3c
style Cache fill:#c8e6c9,stroke:#388e3c
</div>
<h3 id="6-4-data-source-详解">6.4 Data Source 详解</h3>
<h4>Local DataSourcedata/local/</h4>
<ul>
<li><strong>Drift 数据库访问</strong>:使用 packages/storage_sdk 提供的 Drift 封装</li>
<li><strong>本地文件存储</strong>:媒体文件、缓存文件</li>
<li><strong>Secure Storage</strong>敏感数据token、密钥</li>
</ul>
<pre><code>// 示例MessageLocalDataSource
class MessageLocalDataSource {
final DriftSDK _db;
Future&lt;List&lt;MessageDTO&gt;&gt; getMessages(String chatId) {
return _db.query('messages', where: 'chat_id = ?', whereArgs: [chatId]);
}
}
</code></pre>
<h4>Request 文件data/remote/</h4>
<ul>
<li><strong>一个端点 = 一个文件</strong>Response DTO + Request 类放在同一文件中</li>
<li><strong>Repository 直接调 ApiClient</strong>:无需 RemoteDataSource 中间层</li>
<li><strong>@ApiRequest 注解 + 代码生成</strong>:自动实现 path / method / fromJson 注册</li>
</ul>
<pre><code>// 示例Repository 直接调用 Request
// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
final ApiClient _client;
Future&lt;SendMessageData?&gt; sendMessage({
required String chatId,
required String content,
}) {
return _client.executeRequest(
SendMessageRequest(chatId: chatId, content: content),
);
}
}
</code></pre>
<h4>Cache Managerdata/cache/</h4>
<ul>
<li><strong>内存缓存</strong>:频繁访问的数据</li>
<li><strong>缓存策略</strong>TTL、LRU 等策略</li>
<li><strong>缓存失效</strong>:自动或手动失效</li>
</ul>
<h3 id="6-5-dto-models">6.5 DTO Modelsdata/models/</h3>
<p>Data Transfer Objects 用于数据传输:</p>
<ul>
<li><strong>与 API 数据结构对应</strong>JSON 序列化/反序列化</li>
<li><strong>与 Entity 互相转换</strong>DTO ↔ Entity 转换方法</li>
<li><strong>包含数据库映射</strong>Drift 表结构映射</li>
</ul>
<pre><code>// 示例MessageDTO
class MessageDTO {
final String id;
final String content;
final String chatId;
final DateTime timestamp;
// 从 JSON 反序列化
factory MessageDTO.fromJson(Map&lt;String, dynamic&gt; json) { ... }
// 转换为 Entity
Message toEntity() {
return Message(
id: MessageId(id),
content: content,
chatId: ChatId(chatId),
timestamp: timestamp,
);
}
// 从 Entity 转换
factory MessageDTO.fromEntity(Message entity) { ... }
}
</code></pre>
<h3 id="6-6-数据流转">6.6 数据流转</h3>
<div class="mermaid">
flowchart LR
UC[UseCase] -->|调用| RepoInterface[Repository Interface<br/>domain/repositories/]
RepoInterface -.实现.-> RepoImpl[Repository Impl<br/>data/repositories/]
RepoImpl -->|1. 检查缓存| Cache[Cache]
RepoImpl -->|2. 读取本地| LocalDS[Local DS]
RepoImpl -->|3. 请求远程| ApiClient2[ApiClient]
ApiClient2 -->|DTO| RepoImpl
RepoImpl -->|转换| Entity[Entity]
Entity -->|返回| UC
style RepoInterface fill:#f3e5f5,stroke:#7b1fa2
style RepoImpl fill:#e8f5e9,stroke:#388e3c
style UC fill:#f3e5f5,stroke:#7b1fa2
style Entity fill:#f3e5f5,stroke:#7b1fa2
</div>
<blockquote>
<p><strong>关键原则</strong></p>
<p>1. <strong>Data 层是全局目录</strong>:所有 Repository 实现都在 data/repositories/ 下</p>
<p>2. <strong>实现全局接口</strong>:实现 domain/repositories/ 中定义的接口</p>
<p>3. <strong>统一数据源管理</strong>按类型组织local/remote/cache不按 Feature 划分</p>
<p>4. <strong>DTO 与 Entity 分离</strong>DTO 在 Data 层Entity 在 Domain 层</p>
</blockquote>
<hr>
<hr>
<h2 id="core-层模块详解">Core 层模块详解</h2>
<h3 id="7-1-core-层职责">7.1 Core 层职责</h3>
<p>Core 层保留主 App 内部两部分,类似 Apple 的 CoreFoundation / CoreUI 的关系:</p>
<ul>
<li><strong>core/foundation/</strong>Core Foundation—— 应用级非 UI 基础设施:常量、配置、异常、日志、类型、工具函数、扩展</li>
<li><strong>core/ui/</strong>Core UI—— UI 基础设施:基础定义(颜色/字体/间距/主题)、基础组件(按钮/输入框/头像)、业务组合组件(弹窗/面板/Toast</li>
</ul>
<blockquote>
<p>多语言国际化l10n已提取为独立 Package<code>packages/l10n_sdk</code>),与其他 SDK 统一由 Melos 管理。</p>
</blockquote>
<p><strong>依赖方向(严格单向)</strong>core/ui/ → l10n_sdk → core/foundation/。foundation 是最底层l10n_sdk 仅依赖 foundation持久化语言偏好ui 可依赖 l10n_sdk组件内置文案和 foundation。</p>
<h4>core/foundation/Core Foundation包含</h4>
<ul>
<li>常量定义constants</li>
<li>应用配置config</li>
<li>统一异常体系errors</li>
<li>日志系统logger</li>
<li>基础类型types</li>
<li>工具函数utils</li>
<li>Dart 扩展extensions</li>
</ul>
<blockquote>
<p><strong>注意</strong>:网络/存储/加密/媒体/RTC/推送/协议/原生通信/多语言国际化等 SDK 能力已提取为独立 Package<code>packages/*_sdk</code>),由 Melos 统一管理,主 App 通过 <code>pubspec.yaml</code> 引用。详见「<a href="#mono-repo-架构">Mono-Repo 架构</a>」章节和下方「<a href="#7-3-核心-sdk">7.3 核心 SDK独立 Package</a>」。</p>
</blockquote>
<h3 id="7-2-核心-sdk">7.2 基础设施</h3>
<h4>Constantscore/foundation/constants/</h4>
<ul>
<li><code>app_constants.dart</code>:应用级常量(分页大小、超时时间等)</li>
<li><code>db_constants.dart</code>:数据库相关常量(表名、字段名等)</li>
<li><code>socket_constants.dart</code>Socket 相关常量(事件名、重连策略等)</li>
<li><code>api_constants.dart</code>API 相关常量Base URL、端点路径等</li>
</ul>
<h4>Configcore/foundation/config.dart</h4>
<ul>
<li><code>AppConfig</code>:编译期注入的配置聚合类,通过 <code>AppConfig.current</code> 获取当前环境配置</li>
<li>每个字段用 <code>bool.fromEnvironment</code> / <code>String.fromEnvironment</code> 独立读取,新增字段只需在 <code>config.json</code> 和此类各加一行</li>
<li><code>isDebug</code>:核心环境标志(<code>true</code>=dev<code>false</code>=prod由 CI 脚本在打包前写入 <code>config/config.json</code></li>
<li><code>feature_flags.dart</code>:功能开关(灰度 / A/B 实验,后续单独提取)</li>
</ul>
<h4>Errorscore/foundation/errors/</h4>
<ul>
<li><code>app_exception.dart</code>:统一异常基类</li>
<li><code>network_exception.dart</code>:网络异常(超时、断连等)</li>
<li><code>database_exception.dart</code>:数据库异常</li>
<li><code>auth_exception.dart</code>认证异常Token 过期、未授权等)</li>
<li><code>error_mapper.dart</code>:异常 → 错误码 / 错误键映射UI 层通过 l10n 将错误键转为本地化文案)</li>
</ul>
<h4>Loggercore/foundation/logger/</h4>
<ul>
<li><code>app_logger.dart</code>:统一日志门面</li>
<li><code>log_printer.dart</code>:日志格式化输出</li>
<li><code>log_interceptor.dart</code>:网络请求日志拦截器</li>
</ul>
<h4>Typescore/foundation/types/</h4>
<ul>
<li><code>result.dart</code>Result&lt;T&gt; 类型Success / Failure</li>
<li><code>either.dart</code>Either&lt;L, R&gt; 类型</li>
<li><code>typedefs.dart</code>:全局类型别名</li>
<li><code>unit.dart</code>Unit 类型(无返回值占位)</li>
</ul>
<h3 id="7-3-核心-sdk">7.3 核心 SDK独立 Package</h3>
<p>以下 SDK 均为 <code>packages/</code> 下的 Flutter Plugin含 Android/iOS 原生代码入口),主 App 通过 <code>import 'package:xxx_sdk/xxx_sdk.dart'</code> 引用。所有 SDK 遵循 Facade + Wiring 内部架构见「SDK 内部架构」章节)。</p>
<h4>Networks SDKpackages/networks_sdk/</h4>
<p>HTTP + WebSocket 客户端 SDKFlutter Plugin 声明,无实际 Native 代码。App 层通过 <code>import 'package:networks_sdk/networks_sdk.dart'</code> 引用,遵循 Facade + Wiring 内部架构。</p>
<h5>Package 目录结构</h5>
<pre><code>packages/networks_sdk/
├── pubspec.yaml
├── build.yaml # @ApiRequest 代码生成器注册
└── lib/
├── networks_sdk.dart # barrel file统一导出见下方「导出清单」
└── src/
├── annotations/
│ └── api_request.dart # @ApiRequest 注解定义
├── generator/
│ ├── api_request_generator.dart # build_runner 代码生成器实现
│ └── builder.dart # SharedPartBuilder 入口
├── data/
│ ├── datasources/
│ │ ├── http/
│ │ │ ├── api_client.dart # Dio REST 客户端executeRequest&lt;T&gt; 唯一入口)
│ │ │ └── interceptor/
│ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
│ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
│ │ │ └── logging_interceptor.dart # 请求/响应日志
│ │ └── socket/
│ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream 输出)
│ ├── dto/
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
│ └── repositories/
│ ├── networks_sdk_repository_impl.dart
│ └── networks_messaging_repository_impl.dart
├── domain/
│ ├── entities/
│ │ ├── api_error.dart # @freezed HTTP 错误联合类型
│ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
│ │ ├── socket_connection_state.dart # 连接状态 enum
│ │ ├── http_method.dart # GET / POST / PUT / DELETE / PATCH
│ │ └── api_request_type.dart # request / login / upload
│ └── repositories/
│ ├── networks_sdk_repository.dart
│ └── networks_messaging_repository.dart
└── presentation/
├── facade/
│ ├── networks_sdk_api.dart # HTTP 公开 API 接口
│ └── networks_messaging_api.dart # WebSocket 公开 API 接口
└── wiring/
├── api_config.dart # HTTP 配置baseURL / token / 回调)
├── socket_config.dart # WebSocket 配置(心跳 / 重连策略)
├── network_callbacks.dart # 回调类型定义OnTokenRefresh 等)
├── networks_sdk_core.dart
├── networks_sdk_api_impl.dart
├── networks_messaging_api_impl.dart
└── networks_sdk_wiring.dart # 工厂build() / buildMessagingApi()
</code></pre>
<h5>SDK 与 App 层边界</h5>
<table>
<thead>
<tr><th>职责</th><th>SDK (networks_sdk)</th><th>App 层 (im_app)</th></tr>
</thead>
<tbody>
<tr><td>Dio 管理</td><td>ApiClient 内部创建管理</td><td>构造 ApiClient 传入 config</td></tr>
<tr><td>baseURL</td><td>ApiConfig.baseURL</td><td>AppConfig.apiBaseUrl 提供初始值</td></tr>
<tr><td>Token 存储</td><td>ApiConfig.token内存</td><td>安全存储、持久化</td></tr>
<tr><td>Token 刷新</td><td>检测过期 → 调 onTokenRefresh</td><td>提供回调实现</td></tr>
<tr><td>强制登出</td><td>检测条件 → 调 onForceLogout</td><td>提供回调(清状态、跳转登录)</td></tr>
<tr><td>错误码定义</td><td>通用 code != 0 判断</td><td>定义具体业务码传入</td></tr>
<tr><td>请求定义</td><td>ApiRequestable 协议 + @ApiRequest 注解</td><td>各 feature 实现具体 Request</td></tr>
<tr><td>Upload</td><td>uploadData getter + FormData/Uint8List 支持</td><td>override uploadData + decodeResponse</td></tr>
<tr><td>WebSocket 连接</td><td>SocketClient 内部管理(连接/心跳/重连)</td><td>调 connect/disconnect/send</td></tr>
<tr><td>WebSocket 心跳</td><td>双层心跳自动管理(底层 ping 5s + 应用层 10s</td><td>无需关心</td></tr>
<tr><td>WebSocket 重连</td><td>指数退避自动重连1s→2s→4s→8s→16s→30s</td><td>无需关心</td></tr>
<tr><td>WebSocket 生命周期</td><td>提供 onEnterForeground/Background</td><td>App 层调用AppLifecycleListener</td></tr>
<tr><td>WebSocket 消息解析</td><td>JSON.decode → Stream 输出</td><td>App 层按 type 过滤 + DTO 解析</td></tr>
<tr><td>Riverpod</td><td>无依赖</td><td>Provider 包装 ApiClient / SocketClient</td></tr>
</tbody>
</table>
<h5>命名规范(全链路一致性)</h5>
<p>从 Request 文件到 Domain Entity所有文件命名必须遵循统一规则方便区分职责和业务模块。</p>
<table>
<thead>
<tr><th>层级</th><th>文件命名</th><th>类命名</th><th>示例</th></tr>
</thead>
<tbody>
<tr><td>接口定义</td><td><code>{action}_request.dart</code></td><td>Request: <code>{Action}Request</code><br/>Response DTO: <code>{Action}Data</code></td><td><code>login_request.dart</code><code>LoginRequest</code> + <code>LoginData</code></td></tr>
<tr><td>持久化 DTO</td><td><code>data/models/{entity}_dto.dart</code></td><td><code>{Entity}Dto</code></td><td><code>user_dto.dart</code><code>UserDto</code></td></tr>
<tr><td>Repository 接口</td><td><code>domain/repositories/{module}_repository.dart</code></td><td><code>{Module}Repository</code></td><td><code>auth_repository.dart</code><code>AuthRepository</code></td></tr>
<tr><td>Repository 实现</td><td><code>data/repositories/{module}_repository_impl.dart</code></td><td><code>{Module}RepositoryImpl</code></td><td><code>auth_repository_impl.dart</code><code>AuthRepositoryImpl</code></td></tr>
<tr><td>Domain Entity</td><td><code>domain/entities/{entity}.dart</code></td><td><code>{Entity}</code></td><td><code>user.dart</code><code>User</code></td></tr>
<tr><td>UseCase按需</td><td><code>features/{module}/usecases/{action}_usecase.dart</code></td><td><code>{Action}UseCase</code></td><td><code>login_usecase.dart</code><code>LoginUseCase</code></td></tr>
</tbody>
</table>
<p><strong>关键规则</strong></p>
<ul>
<li><strong>一个端点 = 一个 Request 文件</strong>Response DTO + Request 类放在同一文件中</li>
<li><strong>Response DTO 必须有 <code>toEntity()</code></strong>:统一 DTO → Domain Entity 的转换入口</li>
<li><strong>持久化 DTO 和 Response DTO 分开</strong>Response DTO<code>XxxData</code>)在 request 文件中,持久化 DTO<code>XxxDto</code>)在 <code>data/models/</code></li>
<li><strong>禁止跳层</strong>ViewModel → Repository→ UseCase 按需)→ ApiClient每层职责明确</li>
</ul>
<h5>傻瓜式教程:从零开始定义并发送一个接口</h5>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<p style="margin-top: 0; font-weight: 700; color: #388e3c; font-size: 1.1em;">前置条件(只需做一次)</p>
<p>打开一个<strong>独立终端窗口</strong>,在项目根目录执行以下命令并<strong>保持运行,不要关闭</strong></p>
<pre><code class="language-bash">melos run gen:watch</code></pre>
<p style="margin-bottom: 0;">这个命令会监听你的代码变化,<strong>每次保存 .dart 文件后自动生成 .g.dart</strong><br/>
整个开发期间这个终端窗口必须常驻,不要关。</p>
</div>
<p>以「登录接口」为完整示例。假设后端给你的接口文档是:</p>
<pre><code>POST /auth/login
请求体:{ "email": "xxx", "password": "xxx" }
响应体:{ "code": 0, "message": "ok", "data": { "token": "xxx", "user_id": "123", "email": "xxx", "nickname": "xxx", "avatar": "xxx" } }
</code></pre>
<p>你需要做 <strong>4 步</strong>,每一步都告诉你在哪个文件写什么。</p>
<!-- ────────── 第 1 步 ────────── -->
<h6>第 1 步:创建接口定义文件</h6>
<p><strong>在哪创建</strong><code>lib/data/remote/</code> 目录下</p>
<p><strong>文件名</strong><code>login_request.dart</code>(以端点名命名,一个端点 = 一个文件)</p>
<p>规则:<strong>Response DTO + Request 放在同一个文件里</strong>。打开这一个文件就能看到请求参数和响应结构。</p>
<p><strong>第 1.1 步:写文件头</strong></p>
<pre><code class="language-dart">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'; // 这行必须写!指向即将自动生成的文件
</code></pre>
<p><code>part 'login_request.g.dart';</code> 写完后 IDE 会短暂报红(因为 .g.dart 还没生成)。如果 <code>watch</code> 模式已启动,<strong>保存文件后几秒内红线会自动消失</strong></p>
<p><strong>第 1.2 步:写 Response DTO服务端返回什么字段就写什么字段</strong></p>
<pre><code class="language-dart">// ── Response DTO ──
/// 服务端返回的登录数据
@JsonSerializable() // ← 这个注解让 build_runner 自动生成 fromJson / toJson
class LoginData {
final String token; // 服务端返回的字段
@JsonKey(name: 'user_id') // 服务端字段名是 user_idDart 字段名是 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&lt;String, dynamic&gt; json) =&gt;
_$LoginDataFromJson(json); // ← 短暂报红watch 模式下保存后几秒自动消失
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginDataToJson(this);
/// DTO → Domain Entity把网络层数据转为业务层数据
User toEntity() {
return User(
id: userId,
email: email,
nickname: nickname,
avatar: avatar,
);
}
}
</code></pre>
<p><strong>第 1.3 步:在 ApiPaths 中添加路径常量</strong></p>
<p><strong>文件</strong><code>lib/core/foundation/api_paths.dart</code>(全局路径常量表,所有接口路径都在这里统一管理)</p>
<pre><code class="language-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';
// ...
}
</code></pre>
<p>新增接口时,<strong>先在这里加路径常量</strong>,然后在 Request 中引用 <code>ApiPaths.xxx</code><br/>
好处:全局搜索 <code>ApiPaths.</code> 就能看到所有接口列表,路径变更只改一处。</p>
<p><strong>第 1.4 步:写 Request你要发什么参数给服务端</strong></p>
<pre><code class="language-dart">// ── 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&lt;LoginData&gt; // ← 固定写法extends ApiRequestable&lt;响应类型&gt;
with _$LoginRequestApi { // ← 固定写法with _$类名Api短暂报红保存后自动消失
final String email; // 请求参数:要发给服务端的字段
final String password;
LoginRequest({required this.email, required this.password});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginRequestToJson(this); // ← 固定写法,短暂报红,保存后自动消失
}
</code></pre>
<p><strong>保存文件</strong>。后台的 <code>melos run gen:watch</code> 自动检测到变化,几秒后生成 <code>login_request.g.dart</code><strong>所有红线消失</strong></p>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 15px 0;">
<p style="margin-top: 0; font-weight: 700; color: #f57f17;">命名规则速查(写之前就能确定引用名)</p>
<table>
<thead><tr><th>你写的类名</th><th>fromJson</th><th>toJson</th><th>Api mixin</th></tr></thead>
<tbody>
<tr><td><code>LoginData</code></td><td><code>_$LoginDataFromJson</code></td><td><code>_$LoginDataToJson</code></td><td>-</td></tr>
<tr><td><code>LoginRequest</code></td><td><code>_$LoginRequestFromJson</code></td><td><code>_$LoginRequestToJson</code></td><td><code>_$LoginRequestApi</code></td></tr>
<tr><td><code>SendMessageRequest</code></td><td><code>_$SendMessageRequestFromJson</code></td><td><code>_$SendMessageRequestToJson</code></td><td><code>_$SendMessageRequestApi</code></td></tr>
</tbody>
</table>
<p style="margin-bottom: 0;">规则:<code>_$</code> + 类名 + <code>FromJson</code> / <code>ToJson</code> / <code>Api</code>。固定前缀,直接拼。</p>
</div>
<!-- ────────── 第 2 步 ────────── -->
<h6>第 2 步:在 Repository 中调用 ApiClient转为 Domain Entity</h6>
<p><strong>在哪写</strong><code>lib/data/repositories/auth_repository_impl.dart</code></p>
<p><strong>做什么</strong>:直接调 ApiClient.executeRequest → 拿到 DTO → 回调写 Token → 转为 Domain Entity → 返回。</p>
<pre><code class="language-dart">import 'package:networks_sdk/networks_sdk.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../remote/login_request.dart';
class AuthRepositoryImpl implements AuthRepository {
final ApiClient _client; // ← 直接注入 ApiClient
final void Function(String?) _onTokenUpdate; // ← 回调,由 Provider 层组合
AuthRepositoryImpl({
required ApiClient client,
required void Function(String?) onTokenUpdate,
}) : _client = client,
_onTokenUpdate = onTokenUpdate;
@override
Future&lt;User&gt; login({
required String email,
required String password,
}) async {
// 1. 直接调 ApiClient构造请求 → 发 HTTP → 自动解码 → 返回 DTO
final LoginData? loginData = await _client.executeRequest(
LoginRequest(email: email, password: password),
);
if (loginData == null) {
throw Exception('Login failed: empty response');
}
// 2. 回调写入 Token内存 + 持久化由 Provider 层组合)
_onTokenUpdate(loginData.token);
// 3. DTO → Domain Entity返回给上层
return loginData.toEntity();
}
}
</code></pre>
<!-- ────────── 第 3 步 ────────── -->
<h6>第 3 步:注册 Provider + 编写 ViewModel</h6>
<p><strong>3.1 注册 ProviderDI 装配)</strong></p>
<p><strong>在哪写</strong><code>lib/features/{模块}/di/{模块}_providers.dart</code></p>
<p><strong>做什么</strong>:在 Feature 目录下创建 Provider 文件,注册该模块的 DI 链路Repository → UseCase 按需)。<code>app/di/</code> 只提供 SDK 基础设施ApiConfig / ApiClient业务模块的 Provider 内聚在 Feature 目录下。</p>
<pre><code class="language-dart">// ── features/auth/di/auth_providers.dart ──
// Repository直接注入 ApiClient + 回调组合多个 SDK 能力)
final authRepositoryProvider = Provider&lt;AuthRepository&gt;((ref) {
final apiConfig = ref.read(apiConfigProvider);
return AuthRepositoryImpl(
client: ref.read(apiClientProvider), // 直接注入 ApiClient
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存networks_sdk
// secureStorage.saveToken(token); // 持久化storage_sdk待接入
},
);
});
// UseCase按需 — 登录有多步编排,所以需要 UseCase
final loginUseCaseProvider = Provider&lt;LoginUseCase&gt;((ref) {
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
});
</code></pre>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<p style="margin-top: 0; font-weight: 700; color: #388e3c; font-size: 1.1em;">新模块示例:添加「消息」模块的 Provider</p>
<p>假设你新建了 <code>MessageRepositoryImpl</code>,需要注册 Provider</p>
<p>创建 <code>lib/features/chat/di/chat_providers.dart</code></p>
<pre><code class="language-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&lt;MessageRepository&gt;((ref) {
return MessageRepositoryImpl(
client: ref.read(apiClientProvider), // 直接注入 ApiClient
);
});
// 大多数模块只需到 Repository 这一层ViewModel 直接调 Repository 即可。
// 如需 UseCase多步编排、跨模块协调参考 auth_providers.dart 中的 loginUseCaseProvider。
</code></pre>
<p style="margin-bottom: 0;"><strong>原则</strong><code>app/di/</code> 只放 SDK 基础设施ApiConfig / ApiClient业务模块的 DI 链路Repository → UseCase 按需)内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中。</p>
</div>
<p><strong>3.2 编写 ViewModel</strong></p>
<p><strong>在哪写</strong><code>lib/features/auth/presentation/login_view_model.dart</code></p>
<p><strong>常规写法ViewModel 直接调 Repository</strong></p>
<pre><code class="language-dart">@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() =&gt; const LoginState();
Future&lt;void&gt; 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);
}
}
}
</code></pre>
<p><strong>进阶写法:有 UseCase 时(如登录需格式校验 + 多步编排)</strong></p>
<pre><code class="language-dart">@riverpod
class LoginViewModel extends _$LoginViewModel {
@override
LoginState build() =&gt; const LoginState();
Future&lt;void&gt; 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);
}
}
}
</code></pre>
<p>View 层只需 <code>ref.watch(loginViewModelProvider)</code>,状态变化时自动刷新 UI。</p>
<!-- ────────── 完整数据流 ────────── -->
<h6>完整数据流(从用户点击按钮到 UI 刷新)</h6>
<p><strong>常规路径ViewModel → Repository大多数场景</strong></p>
<pre><code>用户点击按钮
→ View: vm.doSomething(...)
→ ViewModel: ref.read(xxxRepositoryProvider).doSomething(...)
→ RepositoryImpl.doSomething() // data/repositories/
→ _client.executeRequest(XxxRequest) // 直接调 ApiClient
→ 自动注入 header → HTTP 请求 → 自动解码 → DTO
→ dto.toEntity() → Domain Entity
← state = state.copyWith(...) // 更新状态
← View: ref.watch → 自动 rebuild // 自动刷新
</code></pre>
<p><strong>进阶路径ViewModel → UseCase → Repository登录等复杂场景</strong></p>
<pre><code>用户点击「登录」按钮
→ View: vm.login(email, password)
→ ViewModel: ref.read(loginUseCaseProvider).execute(...)
→ LoginUseCase: 格式校验(邮箱 + 密码) // features/auth/usecases/
→ LoginUseCase: authRepository.login(...)
→ AuthRepositoryImpl.login() // data/repositories/
→ _client.executeRequest(LoginRequest) // 直接调 ApiClient
→ AuthInterceptor → Dio.request → RetryInterceptor // 自动处理
← request.decodeResponse → LoginData.fromJson // 自动解码
← LoginDataDTO
→ onTokenUpdate(token) // 回调:内存写入 + 持久化
← loginData.toEntity() → UserDomain Entity
← User
← state = state.copyWith(user: user) // 更新状态
← View: ref.watch → 自动 rebuild → UI 显示用户信息 // 自动刷新
</code></pre>
<p>你只写了:接口定义文件 + Repository 一个方法 + ViewModel 一个方法。<br/>
<strong>网络请求、header 注入、token 管理、响应解码、错误处理、JSON 序列化 —— 全部自动完成,你不需要写任何一行。</strong></p>
<!-- ────────── 更多示例 ────────── -->
<h6>再来一个发送消息接口POST /chat/send-message</h6>
<p>后端接口文档:</p>
<pre><code>POST /chat/send-message
请求体:{ "chat_id": "xxx", "content": "hello" }
响应体:{ "code": 0, "data": { "message_id": "456", "timestamp": 1700000000 } }
</code></pre>
<p><strong>你只需创建一个文件</strong><code>lib/data/remote/send_message_request.dart</code>,然后在 Repository 中调用即可。</p>
<pre><code class="language-dart">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&lt;String, dynamic&gt; json) =&gt;
_$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&lt;SendMessageData&gt;
with _$SendMessageRequestApi {
@JsonKey(name: 'chat_id') // JSON 字段名和 Dart 字段名不一样时用 @JsonKey
final String chatId;
final String content;
SendMessageRequest({required this.chatId, required this.content});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$SendMessageRequestToJson(this);
}
</code></pre>
<p>保存 → 自动生成 → 然后在 Repository 中直接调 ApiClient 就完了:</p>
<pre><code class="language-dart">// 在 MessageRepositoryImpl 中添加
Future&lt;SendMessageData?&gt; sendMessage({
required String chatId,
required String content,
}) {
return _client.executeRequest(
SendMessageRequest(chatId: chatId, content: content),
);
}
</code></pre>
<!-- ────────── GET 示例 ────────── -->
<h6>再来一个获取用户资料GET /user/profile</h6>
<pre><code>GET /user/profile ← 无 query 参数,靠 Authorization token 标识当前用户
响应体:{ "code": 0, "data": { "user_id": "123", "email": "tom@example.com", "nickname": "Tom", "avatar": "https://..." } }
</code></pre>
<pre><code class="language-dart">// 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&lt;String, dynamic&gt; json) =&gt;
_$ProfileDataFromJson(json);
User toEntity() =&gt; 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&lt;ProfileData&gt;
with _$GetProfileRequestApi {
GetProfileRequest(); // 无参数 — token 标识当前用户,无需显式传 user_id
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$GetProfileRequestToJson(this);
}
</code></pre>
<!-- ────────── 无响应数据示例 ────────── -->
<h6>无响应数据的接口POST /auth/logout</h6>
<p>有些接口不返回 data 字段,只有 <code>{"code": 0, "message": "ok"}</code>。这种情况用 <code>ApiRequestable&lt;void&gt;</code></p>
<pre><code class="language-dart">// lib/data/remote/logout_request.dart
import 'package:networks_sdk/networks_sdk.dart';
// 无 Response DTO → 泛型写 void
// 不需要 @ApiRequest 注解 → 直接实现 ApiRequestable最简写法
class LogoutRequest extends ApiRequestable&lt;void&gt; {
@override
String get path =&gt; ApiPaths.authLogout;
@override
HttpMethod get method =&gt; HttpMethod.post;
@override
Map&lt;String, dynamic&gt; toJson() =&gt; {}; // 无请求体
}
</code></pre>
<p>Repository 调用时直接 <code>await</code></p>
<pre><code class="language-dart">Future&lt;void&gt; logout() async {
await _client.executeRequest(LogoutRequest()); // 返回 null直接 await 即可
}
</code></pre>
<!-- ────────── Upload 示例 ────────── -->
<h6>文件上传 — 两种模式</h6>
<p>上传与普通请求的核心区别:</p>
<table>
<thead><tr><th>对比项</th><th>普通请求</th><th>Upload 请求</th></tr></thead>
<tbody>
<tr><td>数据来源</td><td><code>toJson()</code> → JSON body</td><td><code>uploadData</code> → FormData / Uint8List</td></tr>
<tr><td>requestType</td><td><code>request</code>(默认)</td><td><code>upload</code></td></tr>
<tr><td>parameters</td><td>有值(自动序列化)</td><td>返回 nullSDK 自动跳过)</td></tr>
<tr><td>响应解码</td><td>标准 <code>{ code, msg, data }</code></td><td>可能需要 override <code>decodeResponse</code></td></tr>
</tbody>
</table>
<p><strong>模式 AFormData 上传到自有后端</strong></p>
<pre><code class="language-dart">// lib/data/remote/upload_file_request.dart
@ApiRequest(
path: ApiPaths.uploadFile,
method: HttpMethod.post,
responseType: UploadResult,
requestType: ApiRequestType.upload, // ← 关键:标记为 upload
)
class UploadFileRequest extends ApiRequestable&lt;UploadResult&gt;
with _$UploadFileRequestApi {
final String filePath;
final String? fileName;
UploadFileRequest({required this.filePath, this.fileName});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; {}; // upload 不走 toJson
/// FormData — SDK 通过 uploadData 获取上传数据
@override
Object? get uploadData =&gt; FormData.fromMap({
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
});
}
</code></pre>
<p><strong>模式 B二进制上传到 S3 presigned URL</strong>(参考 LingoDot-Flutter</p>
<p>先向后端获取 presigned URL再直接上传到 S3</p>
<pre><code class="language-dart">class S3UploadRequest extends ApiRequestable&lt;S3UploadResponse&gt; {
final Uint8List data; // 二进制文件数据
final String presignedURL; // 后端返回的 S3 签名 URL
S3UploadRequest({required this.data, required this.presignedURL});
@override
String get path =&gt; presignedURL; // ← 完整 URLSDK 检测到 http 开头不拼 baseURL
@override
HttpMethod get method =&gt; HttpMethod.put;
@override
ApiRequestType get requestType =&gt; ApiRequestType.upload;
@override
Map&lt;String, String&gt;? get customHeaders =&gt; {'Content-Type': 'application/octet-stream'};
@override
Map&lt;String, dynamic&gt; toJson() =&gt; {};
@override
Object? get uploadData =&gt; data; // Uint8List 直接作为 body
/// S3 返回 204 No Content 或 XML不是标准 { code, msg, data } 信封
/// 必须 override decodeResponse
@override
S3UploadResponse? decodeResponse(Response response) {
if (response.statusCode != null &amp;&amp;
response.statusCode! &gt;= 200 &amp;&amp;
response.statusCode! &lt; 300) {
return const S3UploadResponse(success: true);
}
return const S3UploadResponse(success: false);
}
}
</code></pre>
<!-- ────────── HTTP 方法速查 ────────── -->
<h6>HTTP 方法速查表</h6>
<table>
<thead><tr><th>方法</th><th>示例接口</th><th>参数传递方式</th><th>注解 / 手写</th></tr></thead>
<tbody>
<tr><td>GET</td><td><code>GET /user/profile?user_id=123</code></td><td><code>toJson()</code> → URL query parameters</td><td>@ApiRequest</td></tr>
<tr><td>POST</td><td><code>POST /auth/login</code></td><td><code>toJson()</code> → JSON body</td><td>@ApiRequest</td></tr>
<tr><td>POST无响应</td><td><code>POST /auth/logout</code></td><td><code>toJson()</code> → JSON body → 返回 null</td><td>手写(简单场景)</td></tr>
<tr><td>UploadFormData</td><td><code>POST /upload/file</code></td><td><code>uploadData</code> → FormData</td><td>@ApiRequest + override uploadData</td></tr>
<tr><td>UploadS3</td><td><code>PUT presigned-url</code></td><td><code>uploadData</code> → Uint8List</td><td>手写 + override decodeResponse</td></tr>
<tr><td>PUT / PATCH</td><td><code>PUT /user/profile</code></td><td><code>toJson()</code> → JSON body</td><td>@ApiRequest同 POST</td></tr>
<tr><td>DELETE</td><td><code>DELETE /message/:id</code></td><td><code>toJson()</code> → JSON body 或 query</td><td>@ApiRequest 或手写</td></tr>
</tbody>
</table>
<!-- ────────── 初始化配置 ────────── -->
<h5>App 层初始化配置(已由脚手架创建,通常不需要修改)</h5>
<p><strong>文件位置</strong><code>app/di/network_provider.dart</code>(本文件只提供 SDK 基础设施,不放业务 Provider</p>
<pre><code class="language-dart">/// API 配置 Provider全局单例
/// baseURL 来自 config.json → --dart-define-from-file 编译注入
final apiConfigProvider = Provider&lt;ApiConfig&gt;((ref) {
return ApiConfig(
baseURL: AppConfig.apiBaseUrl,
platformHeaders: {
'Platform': 'Android', // TODO: 运行时从平台 API 获取
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
},
tokenExpiredCodes: {30002, 30003, 30124}, // 后端约定的 Token 过期错误码
forceLogoutCodes: {30125}, // 后端约定的强制登出错误码
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
onTokenRefresh: () async { /* 调刷新 token 接口 */ return null; },
onLog: (message, {tag}) { print('[${tag ?? 'Network'}] $message'); },
);
});
/// API 客户端 Provider全局单例
/// 内部自动挂载 AuthInterceptor / RetryInterceptor / LoggingInterceptor
final apiClientProvider = Provider&lt;ApiClient&gt;((ref) {
final config = ref.read(apiConfigProvider);
return ApiClient(config: config);
});
</code></pre>
<h5>DI 装配总览</h5>
<pre><code>app/di/ ← 手动装配SDK 基础设施
└── network_provider.dart → apiConfigProvider + apiClientProvider
features/{模块}/di/ ← 手动装配:业务模块 DI 链路Repository → UseCase 按需)
├── auth/di/auth_providers.dart → authRepositoryProvider
│ → loginUseCaseProvider按需
├── chat/di/chat_providers.dart → messageRepositoryProvider
│ → sendMessageUseCaseProvider按需
└── ... (需要时才创建,不提前占位)
features/{模块}/presentation/ ← @riverpod 自动生成ViewModel Provider
└── login_view_model.dart → loginViewModelProvider.g.dart 自动生成)
</code></pre>
<p><strong>di/ 目录的定位</strong>:只放<strong>需要手动装配的 Provider</strong>构造注入、回调组合等。ViewModel Provider 由 <code>@riverpod</code> 注解自动生成(写在 <code>presentation/</code> 下),不在 <code>di/</code> 中。</p>
<p><strong>最小化原则</strong><code>app/di/</code> 只提供 SDK 能力ApiConfig / ApiClient不放业务模块的 Provider。每个业务模块的手动装配 Provider 内聚在 <code>features/{模块}/di/{模块}_providers.dart</code> 中,需要时才创建。</p>
<h5>SDK 间解耦:回调注入模式</h5>
<p>Repository 不直接依赖 SDK 类型(如 <code>ApiConfig</code>)。需要 SDK 能力(如写 Token通过回调注入由 Provider 层组合多个 SDK</p>
<pre><code class="language-dart">// features/auth/di/auth_providers.dart — App 层是唯一知道两个 SDK 的地方
final authRepositoryProvider = Provider&lt;AuthRepository&gt;((ref) {
final apiConfig = ref.read(apiConfigProvider);
// final secureStorage = ref.read(secureStorageProvider); // storage_sdk待接入
return AuthRepositoryImpl(
client: ref.read(apiClientProvider), // 直接注入 ApiClient
onTokenUpdate: (token) {
apiConfig.updateToken(token); // 内存networks_sdk
// secureStorage.saveToken(token); // 持久化storage_sdk待接入
},
);
});
</code></pre>
<p><strong>好处</strong><code>networks_sdk</code> 不知道 <code>storage_sdk</code> 的存在,<code>AuthRepositoryImpl</code> 也不依赖任何 SDK 类型——只接收一个 <code>void Function(String?)</code> 回调。各 SDK 保持独立App 层负责组合。</p>
<!-- ────────── 错误处理 ────────── -->
<h5>错误处理</h5>
<p><code>ApiError</code> 是 Freezed 联合类型,覆盖所有网络错误场景。在 ViewModel 的 <code>catch</code> 中使用:</p>
<pre><code class="language-dart">try {
final user = await ref.read(loginUseCaseProvider).execute(...);
} on ApiError catch (e) {
// 方式 A用 displayMessage 一行搞定ApiError 扩展方法,已内置中文提示)
showToast(e.displayMessage);
// 方式 B精细处理每种错误
e.when(
noNetworkConnection: () =&gt; showToast('无网络连接'),
timeout: () =&gt; showToast('请求超时,请重试'),
networkError: (msg) =&gt; showToast('网络错误: $msg'),
decodingError: (msg) =&gt; showToast('数据解析失败'),
apiError: (code, msg) =&gt; showToast('服务端错误[$code]: $msg'),
unknown: (msg) =&gt; showToast('未知错误'),
);
}
</code></pre>
<h5>代码生成(新人必读)</h5>
<p><code>@ApiRequest</code><code>@JsonSerializable</code> 共享同一个 <code>.g.dart</code> 文件SharedPartBuilder无需额外配置。</p>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #1565c0; margin: 20px 0;">
<p style="margin-top: 0; font-weight: 700; color: #1565c0; font-size: 1.1em;">开发流程3 步)</p>
<p><strong>第 0 步:启动监听(整个开发期间只需执行一次,在独立终端窗口常驻)</strong></p>
<pre><code class="language-bash"># ⚠️ 强制要求:开发期间必须常驻此命令
# 在项目根目录打开一个独立终端窗口,执行:
melos run gen:watch
</code></pre>
<p>启动后,<strong>每次保存 .dart 文件都会自动重新生成 .g.dart</strong>,无需手动操作。</p>
<p><strong>第 1 步:手写源文件</strong></p>
<pre><code class="language-dart">// 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&lt;String, dynamic&gt; json) =&gt;
_$LoginDataFromJson(json);
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginDataToJson(this);
}
// ── Request ──
@ApiRequest(
path: ApiPaths.authLogin,
method: HttpMethod.post,
responseType: LoginData,
requestType: ApiRequestType.login,
)
@JsonSerializable()
class LoginRequest extends ApiRequestable&lt;LoginData&gt;
with _$LoginRequestApi { // ← 短暂报红,保存后自动消失
final String email;
final String password;
LoginRequest({required this.email, required this.password});
@override
Map&lt;String, dynamic&gt; toJson() =&gt; _$LoginRequestToJson(this); // ← 短暂报红,保存后自动消失
}
</code></pre>
<p><strong>第 2 步:保存文件 → 自动生成</strong></p>
<p>保存后,后台的 <code>melos run gen:watch</code> 自动检测到文件变化,生成 <code>login_request.g.dart</code><br/>
<strong>所有红线自动消失</strong>,无需任何手动操作。</p>
</div>
<div style="background: #fce4ec; padding: 15px; border-radius: 8px; border-left: 4px solid #c62828; margin: 20px 0;">
<p style="margin-top: 0; font-weight: 700; color: #c62828;">命名规则(写之前就能确定引用名)</p>
<table>
<thead><tr><th>注解</th><th>生成的符号</th><th>示例</th></tr></thead>
<tbody>
<tr><td><code>@JsonSerializable()</code></td><td><code>_$类名FromJson()</code></td><td><code>_$LoginDataFromJson(json)</code></td></tr>
<tr><td><code>@JsonSerializable()</code></td><td><code>_$类名ToJson()</code></td><td><code>_$LoginDataToJson(this)</code></td></tr>
<tr><td><code>@ApiRequest(...)</code></td><td><code>_$类名Api</code>mixin</td><td><code>_$LoginRequestApi</code></td></tr>
</tbody>
</table>
<p>规则固定,先写引用再保存,<strong>watch 模式下几秒后红线自动消失</strong>。如果红线没消失,检查 watch 终端是否在运行。</p>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p style="margin-top: 0; font-weight: 700; color: #f57f17;">常见问题</p>
<ul style="margin-bottom: 0;">
<li><strong>忘了启动 watch</strong>:保存后红线不消失 → 检查终端是否有 <code>melos run gen:watch</code> 在运行</li>
<li><strong>生成报错</strong><code>melos run gen</code> 重新全量生成</li>
<li><strong>.g.dart 冲突</strong>:多人协作时 .g.dart 冲突直接删除后重新生成即可,不要手动合并</li>
<li><strong>新增依赖后</strong>:先 <code>dart pub get</code>,再重启 watch</li>
</ul>
</div>
<h4>Storage SDKpackages/storage_sdk/</h4>
<p>纯基础设施 SDK不感知业务表结构。遵循 Facade + Wiring 模式,结构同 cipher_guard_sdk。</p>
<p><strong>职责边界:</strong></p>
<ul>
<li><strong>storage_sdk 负责</strong>:数据库连接生命周期(按 uid 隔离文件)、通用泛型 CRUDinsert / select / watch / rawQuery 等)</li>
<li><strong>im_app 负责</strong>AppDatabase 定义(含表声明和迁移策略)、各业务表(<code>data/local/drift/tables/</code></li>
</ul>
<p><strong>使用方式:</strong></p>
<pre><code>// app/di/db_provider.dart
final storageSdkProvider = Provider&lt;StorageSdkApi&gt;((ref) {
return StorageSdkApi(
databaseFactory: (executor) =&gt; 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) =&gt; t.uid.equals(uid));
</code></pre>
<p><strong>公开接口StorageSdkApi</strong>生命周期openDatabase / closeDatabase / isDatabaseOpen+ 泛型 CRUDinsertOrReplace / batchInsertOrReplace / updateWhere / deleteWhere / deleteAll / selectAll / selectWhere / selectFirst / watchAll / watchWhere / watchFirst / rawQuery / rawExecute / count</p>
<p><strong>build.yamlim_app</strong>apps/im_app/build.yaml 配置 <code>drift_dev|preparing_builder: generate_for: [lib/**]</code>,确保 gen:watch 在修改表文件(无注解的 .dart时正确触发 app_database.g.dart 重新生成。</p>
<h4>Media SDKpackages/media_sdk/</h4>
<p>遵循 Facade + Wiring 模式。负责图片/视频处理,具体功能实现待开发。</p>
<h4>RTC SDKpackages/rtc_sdk/</h4>
<p>遵循 Facade + Wiring 模式。负责实时音视频WebRTC具体功能实现待开发。</p>
<h4>Push SDKpackages/notification_sdk/</h4>
<p>遵循 Facade + Wiring 模式。负责推送通知FCM / APNs具体功能实现待开发。</p>
<h4>Protocol SDKpackages/protocol_sdk/</h4>
<p>遵循 Facade + Wiring 模式。负责消息协议Protobuf 序列化),具体功能实现待开发。</p>
<h4>CipherGuard SDKpackages/cipher_guard_sdk/)—— Flutter Plugin</h4>
<p>端对端加密 SDK同时处理 Dart 侧加解密和 Native 侧密钥同步iOS App Group 用于推送通知解密):</p>
<ul>
<li><code>cipher_guard_sdk_api.dart</code>:公开 API 接口Facade</li>
<li><code>encryption_flutter_service.dart</code>RSA/AES 双层加解密(纯 Dart 实现,基于 pointycastle + encrypt</li>
<li><code>encryption_method_channel.dart</code>Native 密钥同步通道iOS App Group 共享密钥供 Notification Extension 解密)</li>
<li>Domain 实体:<code>RsaKeyPair</code> / <code>SessionKey</code> / <code>EncryptedMessage</code> / <code>ChatEncryptionKey</code></li>
<li><code>android/</code> + <code>ios/</code>Plugin 注册入口,原生侧实现密钥写入 App Group</li>
</ul>
<h3 id="7-4-l10n">7.4 多语言国际化packages/l10n_sdk/</h3>
<p>已提取为独立 Package被 core/ui 和 Feature 层单向引用foundation 不依赖它)。</p>
<p><strong>核心策略:远端动态下载,一劳永逸</strong></p>
<p>翻译文案不随包发布App 启动后自动从远端 URL 拉取最新版本并写入共享空间Dart 层和原生层Android / iOS均从共享空间读取。后续只需在多语言后台追加/修改文案,无需发版,立即生效。</p>
<h4>启动流程</h4>
<ol>
<li><strong>拉取远端翻译</strong><code>l10n_loader.dart</code> 在 App 初始化阶段请求远端 URL下载当前语言的翻译 JSON</li>
<li><strong>写入共享空间</strong>将下载结果持久化到跨层共享目录Documents / App Group ContainerDart 层和原生层均可读取</li>
<li><strong>加载入口</strong><code>l10n.dart</code> 优先从共享空间读取翻译,命中则使用;未命中则回退到内置兜底文件</li>
<li><strong>原生层同步</strong>Android / iOS 原生代码同样从共享空间读取,与 Dart 层使用同一份翻译数据</li>
</ol>
<h4>兜底机制</h4>
<ul>
<li>首次启动(远端尚未下载):使用 <code>assets/fallback_*.json</code> 内置兜底翻译,保证界面不出空白</li>
<li>网络异常:继续使用上次成功缓存的翻译,下次启动重试</li>
<li>远端返回格式错误:忽略,保持当前缓存</li>
</ul>
<h4>模块职责</h4>
<ul>
<li><code>l10n_loader.dart</code>:远端拉取 + 写入共享空间App 启动时调用一次</li>
<li><code>l10n.dart</code>:多语言访问入口,优先读共享空间,回退至内置兜底</li>
<li><code>locale_provider.dart</code>:语言切换管理 —— 当前 Locale 状态、用户偏好持久化、跟随系统语言</li>
<li><code>assets/fallback_*.json</code>:内置兜底翻译,随包发布,仅作离线保障</li>
</ul>
<blockquote>
<p><strong>为什么独立 Package</strong>:国际化服务于 core/ui组件内置文案和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。<strong>注意</strong>foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。</p>
</blockquote>
<h3 id="7-5-core-ui">7.5 Core UIcore/ui/</h3>
<p>UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:</p>
<h4>第一层基础定义core/ui/base/</h4>
<p>最底层的视觉规范定义,<strong>不含任何 Widget</strong>,只输出颜色/字体常量和 ThemeData</p>
<ul>
<li><code>colors.dart</code>(已实现):颜色体系 —— 品牌色、语义色success / warning / error、中性灰阶</li>
<li><code>font.dart</code>(已实现):字体 —— TextStyle 定义 + <code>textTheme(brightness)</code>(统一字族/字号/行高)</li>
<li><code>app_theme.dart</code>(已实现):主题组装 —— 将以上令牌组合为 Light / Dark ThemeData</li>
<li>spacing / radius / shadows 等(待开发,按需添加)</li>
</ul>
<h4>第二层基础组件core/ui/components/</h4>
<p>原子级 Widget只依赖第一层 base<strong>不含任何业务逻辑</strong></p>
<ul>
<li><code>app_button.dart</code>已实现按钮Primary / Secondary / Text 变体)</li>
<li>app_text_field / app_avatar / app_badge 等(待开发)</li>
</ul>
<h4>第三层业务组合组件core/ui/composites/</h4>
<p>由 base + components 组合而成的高阶 Widget封装通用业务交互模式</p>
<ul>
<li><code>app_dialog.dart</code>已实现确认弹窗title / content / 确认 / 取消按钮)</li>
<li>app_action_sheet / app_toast / app_empty_state / app_error_view 等(待开发)</li>
</ul>
<blockquote>
<p><strong>依赖方向</strong>composites → components → base → core/foundation/(颜色/字体等可以引用 foundation 的 config。composites 可引用 l10n_sdk组件内置文案。Feature 层只引用 core/ui/,不直接使用 Flutter 原生 Material 组件。</p>
<p><strong>依赖链</strong>core/ui/ → l10n_sdk → core/foundation/,严格单向,不可反向依赖。</p>
</blockquote>
<h3 id="sdk-约束与管理">SDK 约束与管理</h3>
<h4>使用 Melos 实现 Mono-Repo</h4>
<p>为了有效管理多个 SDK 和保证版本一致性,我们使用 <strong>Melos + mono-repo</strong> 架构。</p>
<h4>Melos + mono-repo 的优势</h4>
<table>
<thead><tr>
<th>维度</th>
<th>Melos + mono-repo</th>
<th>传统 multi-repo</th>
</tr></thead>
<tbody>
<tr>
<td><strong>版本一致性</strong></td>
<td>同一个 commit 保证所有 package 兼容</td>
<td>版本靠人同步,常出现 mismatch</td>
</tr>
<tr>
<td><strong>API 变更</strong></td>
<td>编译期立即发现,马上修复</td>
<td>发版后才发现debug 成本高</td>
</tr>
<tr>
<td><strong>Refactor 成本</strong></td>
<td>一次性全 repo refactor</td>
<td>需要跨 repo、分批跟进</td>
</tr>
<tr>
<td><strong>依赖关系管理</strong></td>
<td>Melos 自动解析、link 本地套件</td>
<td>pub / git tag 人工管理</td>
</tr>
<tr>
<td><strong>SDK 开发体验</strong></td>
<td>改 SDK → example app 立即验证</td>
<td>必须先发版才能验证</td>
</tr>
<tr>
<td><strong>CI / 指令一致性</strong></td>
<td>一套 melos 指令走天下</td>
<td>每个 repo 一套 script</td>
</tr>
<tr>
<td><strong>测试策略</strong></td>
<td>可只测受影响的 packages</td>
<td>常常只能全测或凭感觉</td>
</tr>
<tr>
<td><strong>Debug 效率</strong></td>
<td>问题可回溯到单一 commit</td>
<td>问题横跨多个 repo / 版本</td>
</tr>
<tr>
<td><strong>新人上手</strong></td>
<td>clone 一个 repo 就全到位</td>
<td>要 clone 多个 repo 才能跑</td>
</tr>
<tr>
<td><strong>技术债累积</strong></td>
<td>缓慢、可控</td>
<td>指数型成长</td>
</tr>
</tbody></table>
<h4>SDK 约束规则</h4>
<ol>
<li><strong>位置约束</strong>:所有可复用 SDK 在 <code>packages/</code> 独立 Package应用级基础设施在 <code>core/foundation/</code>UI 基础设施在 <code>core/ui/</code></li>
<li><strong>依赖约束</strong>SDK 之间不能相互依赖(除非明确声明)</li>
<li><strong>职责约束</strong>SDK 只提供纯技术能力,不包含业务逻辑</li>
<li><strong>版本管理</strong>:使用 Melos 统一管理版本和依赖</li>
<li><strong>独立性</strong>:每个 SDK 可独立测试、发布</li>
</ol>
<p>环境配置、初始化步骤和 Melos 命令速查表见文档顶部 <a href="#part0-setup">Part 0开发环境配置</a></p>
<hr>
<hr>
<h2 id="扩展性设计">扩展性设计</h2>
<h3 id="8-1-新增-feature">8.1 新增 Feature</h3>
<p>添加新功能的标准流程:</p>
<ol>
<li><code>features/</code> 下创建新目录</li>
<li>创建 UI 层页面</li>
<li>创建 Presentation 层 ViewModel</li>
<li>创建 Domain 层 UseCase</li>
<li>在 Domain 层定义 Repository 接口</li>
<li>在 Data 层实现 Repository</li>
<li><code>features/{模块}/di/{模块}_providers.dart</code> 中注册模块 Provider</li>
</ol>
<h3 id="8-2-标准-feature-结构模板">8.2 标准 Feature 结构模板</h3>
<p>每个新 Feature 都应遵循以下标准结构:</p>
<h4>两档模板选择指南</h4>
<table>
<thead>
<tr><th>复杂度</th><th>适用场景</th><th>必须有</th><th>可选添加</th></tr>
</thead>
<tbody>
<tr>
<td><strong>简单</strong></td>
<td>search、settings、profile 等</td>
<td>view/ + presentation/vm + state</td>
<td>di/(需要自定义 Provider 时)</td>
</tr>
<tr>
<td><strong>标准</strong></td>
<td>chat、call、auth 等复杂功能</td>
<td>view/ + presentation/ + di/</td>
<td>usecases/(多步编排、跨模块协调时按需添加)</td>
</tr>
</tbody>
</table>
<h4>简单模板search / settings / profile 类)</h4>
<pre><code>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 # 代码生成
</code></pre>
<h4>标准模板chat / call / auth 类)</h4>
<pre><code>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
</code></pre>
<p><strong>Domain Entity 说明</strong>共享实体Message、Contact、User 等)统一放在全局 <code>domain/entities/</code>,不在 feature 内部定义。</p>
<h4>完整示例:创建 Profile Feature简单模板</h4>
<div class="mermaid">
flowchart TD
Step1[1. 创建目录结构<br/>features/profile/]
Step2[2. 创建 UI 层<br/>profile/view/profile_page.dart]
Step3[3. 创建 Presentation 层<br/>profile/presentation/profile_view_model.dart]
Step4[4. 定义 Repository 接口<br/>domain/repositories/profile_repository.dart]
Step5[5. 实现 Repository<br/>data/repositories/profile_repository_impl.dart]
Step6[6. 注册 Provider<br/>features/profile/di/profile_providers.dart]
StepOpt[可选:提取 UseCase<br/>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
</div>
<h4>具体步骤</h4>
<p><strong>步骤 1创建 Feature 目录</strong></p>
<pre><code>lib/features/profile/
├── view/
└── presentation/
</code></pre>
<p><strong>步骤 2创建 UI 层页面(使用 ConsumerWidget</strong></p>
<pre><code>// 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,
),
);
}
}
</code></pre>
<p><strong>步骤 3创建 Presentation 层 ViewModel直接调 Repository</strong></p>
<pre><code>// 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() =&gt; const ProfileState();
Future&lt;void&gt; 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&lt;void&gt; 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);
}
}
}
</code></pre>
<p><strong>步骤 4在全局 Domain 层定义 Repository 接口</strong></p>
<pre><code>// domain/repositories/profile_repository.dart
abstract class ProfileRepository {
Future&lt;Profile&gt; getProfile();
Future&lt;void&gt; updateProfile(Profile profile);
}
</code></pre>
<p><strong>步骤 5在 Data 层实现 Repository</strong></p>
<pre><code>// data/repositories/profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepository {
final ApiClient _client;
ProfileRepositoryImpl({required ApiClient client})
: _client = client;
@override
Future&lt;Profile&gt; getProfile() async {
final data = await _client.executeRequest(GetProfileRequest());
return data!.toEntity();
}
@override
Future&lt;void&gt; updateProfile(Profile profile) async {
// 实现逻辑
}
}
</code></pre>
<p><strong>步骤 6在 Feature 目录下注册模块 Provider</strong></p>
<p>创建 <code>features/profile/di/profile_providers.dart</code>,注册 DI 链路:</p>
<pre><code class="language-dart">// features/profile/di/profile_providers.dart
import '../../../app/di/network_provider.dart';
// ── Repository ──
final profileRepositoryProvider = Provider&lt;ProfileRepository&gt;((ref) {
return ProfileRepositoryImpl(
client: ref.read(apiClientProvider), // 直接注入 ApiClient
);
});
// Profile 是简单 CRUD不需要 UseCase。
// ViewModel 通过 @riverpod 注解自动生成 Provider无需额外注册。
</code></pre>
<p><strong>说明</strong>Profile 属于简单模板ViewModel 直接调 Repository无需 UseCase 中间层。<code>app/di/</code> 只提供 SDK 基础设施ApiConfig / ApiClient业务模块的 DI 链路内聚在 Feature 目录下。</p>
<h4>Feature 结构图示</h4>
<div class="mermaid">
flowchart TD
subgraph ProfileFeature[Profile Feature - 垂直切片]
UI[UI Layer<br/>profile/view/profile_page.dart]
Presentation[Presentation Layer<br/>profile/presentation/profile_view_model.dart]
DI[DI 装配<br/>profile/di/profile_providers.dart]
end
subgraph GlobalLayers[全局层]
RepoInterface[Repository Interface<br/>domain/repositories/profile_repository.dart]
RepoImpl[Repository Implementation<br/>data/repositories/profile_repository_impl.dart]
SDKs[SDK Packages<br/>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
</div>
<blockquote>
<p><strong>关键要点</strong></p>
<p>1. <strong>垂直切片</strong>:每个 Feature 包含 UI → Presentation→ UseCase 按需)的链路</p>
<p>2. <strong>高内聚</strong>Feature 内部的代码都在同一目录下,便于维护</p>
<p>3. <strong>低耦合</strong>ViewModel 通过 Repository 接口与数据层解耦</p>
<p>4. <strong>渐进式</strong>:从简单模板起步,业务复杂时按需升级为标准模板</p>
</blockquote>
<h3 id="8-3-替换底层实现">8.3 替换底层实现</h3>
<p>由于依赖倒置原则,可以轻松替换底层实现:</p>
<ul>
<li>替换数据库:只需修改 Drift SDK</li>
<li>替换网络库:只需修改 Network SDK</li>
<li>替换加密算法:只需修改 Crypto SDK</li>
</ul>
<h3 id="8-4-跨平台扩展">8.4 跨平台扩展</h3>
<ul>
<li>Platform Adapters 处理平台差异</li>
<li>SDK Packages 提供统一接口</li>
<li>Platform-specific 实现在各自的 SDK Package 中(如 cipher_guard_sdk 的 android/ / ios/</li>
</ul>
<hr>
<hr>
<h2 id="项目配置">项目配置</h2>
<h3 id="10-1-pubspec-yaml-依赖">10.1 pubspec.yaml 依赖</h3>
<h4>核心依赖</h4>
<pre><code>name: im_app
description: IM Application with Clean Architecture
version: 1.0.0+1
environment:
sdk: '&gt;=3.0.0 &lt;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
</code></pre>
<h4>依赖说明</h4>
<table>
<thead>
<tr>
<th>类别</th>
<th>包名</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>状态管理</strong></td>
<td>flutter_riverpod</td>
<td>Riverpod 核心库</td>
</tr>
<tr>
<td></td>
<td>riverpod_annotation</td>
<td>Riverpod 注解,用于代码生成</td>
</tr>
<tr>
<td><strong>不可变状态</strong></td>
<td>freezed_annotation</td>
<td>Freezed 注解,生成不可变类</td>
</tr>
<tr>
<td><strong>代码生成</strong></td>
<td>build_runner</td>
<td>Dart 代码生成工具</td>
</tr>
<tr>
<td></td>
<td>riverpod_generator</td>
<td>Riverpod Provider 代码生成</td>
</tr>
<tr>
<td></td>
<td>freezed</td>
<td>Freezed 代码生成</td>
</tr>
<tr>
<td></td>
<td>json_serializable</td>
<td>JSON 序列化代码生成</td>
</tr>
<tr>
<td><strong>网络</strong></td>
<td>dio</td>
<td>HTTP 客户端</td>
</tr>
<tr>
<td></td>
<td>web_socket_channel</td>
<td>WebSocket 通信</td>
</tr>
<tr>
<td><strong>存储</strong></td>
<td>drift</td>
<td>类型安全的响应式数据库(基于 SQLite</td>
</tr>
<tr>
<td></td>
<td>flutter_secure_storage</td>
<td>安全存储(加密)</td>
</tr>
<tr>
<td><strong>测试</strong></td>
<td>mocktail</td>
<td>Mock 测试工具</td>
</tr>
</tbody>
</table>
<h3 id="10-2-代码生成命令">10.2 代码生成命令</h3>
<p>项目使用 Melos 统一管理,所有代码生成命令通过 <code>melos run</code> 执行,会自动作用于所有依赖 <code>build_runner</code> 的 Package。</p>
<h4>一次性生成</h4>
<p>用于 CI 流水线,或首次 clone 后手动触发一次:</p>
<pre><code>melos run gen
</code></pre>
<h4>监听模式(开发期间必开)</h4>
<p>开发时需要在一个<strong>独立的 Terminal 窗口</strong>中启动,全程保持运行。每次保存 <code>.dart</code> 文件后build_runner 会自动检测变化并重新生成对应的 <code>.g.dart</code> / <code>.freezed.dart</code> 文件,无需手动执行。</p>
<pre><code># 在独立 Terminal 窗口执行,不要关闭
melos run gen:watch
</code></pre>
<p>⚠️ 若忘记开启,修改了 <code>@freezed</code> / <code>@JsonSerializable</code> 等注解后不会自动生成,编译时会报找不到对应文件的错误。</p>
<h4>底层等价命令(参考)</h4>
<p>Melos 实际代为执行的命令,无需手动调用:</p>
<pre><code>dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch --delete-conflicting-outputs
</code></pre>
<h3 id="10-3-main-dart-入口配置">10.3 环境配置与启动入口</h3>
<h4>设计思路</h4>
<p>采用 <strong>单一配置文件 + CI 脚本写入</strong> 的方式管理多环境配置:</p>
<ul>
<li><code>config/config.json</code> 提交到 Git默认存 dev 值(<code>IS_DEV=true</code></li>
<li>CI 打包线上版本时,脚本直接改写此文件写入 prod 值,再执行 <code>flutter build</code></li>
<li>Dart 通过 <code>--dart-define-from-file</code> 在编译期将 JSON 字段注入二进制,运行时零开销读取</li>
</ul>
<h4>config/config.json提交到 Git默认 dev</h4>
<pre><code>{
"IS_DEV": true,
"API_BASE_URL": "https://dev-api.example.com"
}
</code></pre>
<p>后续新增字段WebSocket 地址、Sentry DSN、第三方 SDK Key 等)直接在此文件加 Key 即可,无需改启动逻辑。</p>
<h4>core/foundation/config.dart</h4>
<pre><code>// 编译期从 --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 =&gt; const AppConfig(
isDebug: _kIsDebug,
apiBaseUrl: _kApiBaseUrl,
);
final bool isDebug;
final String apiBaseUrl;
bool get isProd =&gt; !isDebug;
}
</code></pre>
<h4>app/bootstrap.dart</h4>
<pre><code>void bootstrap(AppConfig config) {
WidgetsFlutterBinding.ensureInitialized();
runApp(IMApp(config: config));
}
</code></pre>
<h4>main.dart</h4>
<pre><code>void main() {
bootstrap(AppConfig.current);
}
</code></pre>
<h4>Android Studio / VSCode 运行配置</h4>
<p>在 Android Studio 的 Run/Debug Configurations → Additional run args 中配置:</p>
<table>
<thead><tr><th>配置名</th><th>Additional run args</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>im-debug</code></td><td><code>--dart-define-from-file=config/config.json --debug</code></td><td>本地开发调试</td></tr>
<tr><td><code>im-release</code></td><td><code>--dart-define-from-file=config/config.json --release</code></td><td>本地 Release 模式验证</td></tr>
</tbody>
</table>
<p>路径 <code>config/config.json</code> 相对于 Flutter 模块根目录(<code>apps/im_app/</code>)。配置已保存在 <code>.idea/runConfigurations/</code>clone 后 Android Studio 可直接使用。</p>
<h4>打包脚本</h4>
<p>每个平台均有独立的打包脚本,统一放在 <code>scripts/</code> 目录下,并通过 melos 命令调用。所有脚本均启用 <code>--split-debug-info</code> + <code>--obfuscate</code> 以减少产物体积。</p>
<table>
<thead><tr><th>平台</th><th>脚本</th><th>melos 命令</th><th>用途</th><th>产物</th></tr></thead>
<tbody>
<tr>
<td rowspan="2">Android</td>
<td rowspan="2"><code>scripts/build_android.sh</code></td>
<td><code>melos run build:android:apk</code></td>
<td>本地测试 / 内部分发</td>
<td><code>build/app/outputs/flutter-apk/app-release.apk</code></td>
</tr>
<tr>
<td><code>melos run build:android:aab</code></td>
<td>Google Play 上架</td>
<td><code>build/app/outputs/bundle/release/app-release.aab</code></td>
</tr>
<tr>
<td>iOS</td>
<td><code>scripts/build_ios.sh</code></td>
<td><code>melos run build:ios</code></td>
<td>App Store / 内部分发</td>
<td><code>build/ios/ipa/im_app.ipa</code></td>
</tr>
<tr>
<td>macOS</td>
<td><code>scripts/build_macos.sh</code></td>
<td><code>melos run build:macos</code></td>
<td></td>
<td><code>build/macos/Build/Products/Release/im_app.app</code></td>
</tr>
<tr>
<td>Windows</td>
<td><code>scripts/build_windows.sh</code></td>
<td><code>melos run build:windows</code></td>
<td></td>
<td><code>build/windows/x64/runner/Release/</code></td>
</tr>
</tbody>
</table>
<p><strong>melos 调用方式</strong></p>
<pre><code>melos run build:android:apk # APK本地测试
melos run build:android:aab # AABGoogle 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
</code></pre>
<p><strong>体积优化说明</strong></p>
<ul>
<li><code>--split-debug-info</code>:将 Dart 调试符号从主产物中剥离,存入 <code>build/debug-info/&lt;platform&gt;/</code>,可减少 10~20 MB</li>
<li><code>--obfuscate</code>:混淆 Dart 符号名称,需配合符号表还原线上崩溃堆栈</li>
<li>Android 额外优化:仅编译 <code>arm64-v8a</code>、R8 代码压缩(<code>isMinifyEnabled</code>)、资源压缩(<code>isShrinkResources</code></li>
<li>符号表请妥善保存,与对应版本一一对应,用于线上崩溃堆栈还原</li>
</ul>
<h3 id="10-4-analysis-options-yaml">10.4 analysis_options.yaml</h3>
<pre><code>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
</code></pre>
<h3 id="10-5-ci-workflow">10.5 CI WorkflowGitea Actions</h3>
<p><strong>文件:<code>.github/workflows/ci.yml</code></strong></p>
<pre><code class="language-yaml">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
</code></pre>
<p><strong>步骤说明</strong></p>
<table>
<thead><tr><th>步骤</th><th>命令</th><th>说明</th></tr></thead>
<tbody>
<tr><td>Deep clean</td><td><code>melos run clean:deep</code></td><td>清除全平台缓存Flutter / Android Gradle / iOS Pods / macOS Pods / Windows CMake及所有生成文件确保 CI 环境干净</td></tr>
<tr><td>Install dependencies</td><td><code>dart pub get</code></td><td>在根目录统一解析所有 package 依赖,生成单一 pubspec.lockDart pub workspace</td></tr>
<tr><td>Generate code</td><td><code>melos run gen</code></td><td>生成 <code>.g.dart</code> / <code>.freezed.dart</code><code>*.g.dart</code> 不提交CI 每次重新生成)</td></tr>
<tr><td>Analyze</td><td><code>melos run analyze</code></td><td>对所有 package 执行静态分析lint 不通过则 PR 不可合并</td></tr>
</tbody>
</table>
<p><strong>触发规则</strong></p>
<ul>
<li><code>pull_request</code><code>main</code> / <code>dev</code>PR 提交或更新时运行,必须通过才能合并</li>
<li><code>push</code><code>main</code> / <code>dev</code>PR 合并后触发(两个分支均开启 branch protection不允许直接 push</li>
</ul>
<p><strong>打包策略</strong></p>
<p>打包不在自动 CI 中触发,通过 <strong>IM 管理后台</strong>手动触发打包任务。打包 workflow 单独维护,与 lint/analyze 流水线解耦。</p>
<p><strong>预留 CI 能力</strong></p>
<table>
<thead><tr><th>能力</th><th>触发时机</th><th>状态</th><th>说明</th></tr></thead>
<tbody>
<tr>
<td>AI 代码 Review</td>
<td>PR 提交 / 更新时</td>
<td>🔜 预留</td>
<td>对每个 PR 的 diff 调用 AI 接口,自动输出可读性、架构合规性、潜在问题等 Review 意见,以 PR 评论形式呈现</td>
</tr>
</tbody>
</table>
<p><strong>分支保护建议Gitea → Settings → Branches</strong></p>
<ul>
<li>Require a pull request before merging</li>
<li>Require status checks to pass<code>Lint</code></li>
<li>Do not allow bypassing the above settings</li>
</ul>
<hr>
<hr>
<h2 id="part5-examples" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第五部分:数据流转示例</h2>
<h3 id="9-1-发送消息流程">9.1 发送消息流程Feature 驱动)</h3>
<p>下面展示一个完整的发送消息流程,说明数据如何在 Feature 驱动的架构中流转:</p>
<div class="mermaid">
sequenceDiagram
participant UI as features/chat/view/<br/>chat_page.dart
participant VM as features/chat/presentation/<br/>chat_view_model.dart
participant UC as features/chat/usecases/<br/>send_message_usecase.dart
participant Repo as domain/repositories/<br/>message_repository.dart
participant RepoImpl as data/repositories/<br/>message_repository_impl.dart
participant LocalDS as data/local/<br/>message_local_ds.dart
participant SDK as networks_sdk/<br/>ApiClient / SocketClient
participant WS as WebSocket Server
UI->>VM: 1. 用户点击发送按钮
VM->>UC: 2. 调用 SendMessageUseCase
UC->>Repo: 3. 调用 Repository 接口
Repo->>RepoImpl: 4. Data 层实现接口
RepoImpl->>LocalDS: 5. 先保存到本地数据库
LocalDS-->>RepoImpl: 6. 本地保存成功(设置状态:发送中)
RepoImpl->>SDK: 7. 直接调 SDK 发送
SDK->>WS: 8. 发送消息到服务器
WS-->>SDK: 9. 服务器确认收到
SDK-->>RepoImpl: 10. 返回发送结果
RepoImpl->>LocalDS: 11. 更新本地消息状态(已发送)
LocalDS-->>RepoImpl: 12. 更新成功
RepoImpl-->>UC: 13. 返回 Message Entity
UC-->>VM: 14. 返回结果
VM-->>UI: 15. 更新 UI 状态
UI->>UI: 16. 显示消息已发送
Note over UI,WS: Feature 垂直切片chat Feature 的完整数据流
</div>
<h3 id="9-2-流程说明">9.2 流程说明</h3>
<ol>
<li><strong>用户操作</strong>:用户在 <code>features/chat/view/chat_page.dart</code> 点击发送按钮</li>
<li><strong>ViewModel 响应</strong><code>features/chat/presentation/chat_view_model.dart</code> 处理发送逻辑</li>
<li><strong>调用 UseCase</strong>ViewModel 调用 <code>features/chat/usecases/send_message_usecase.dart</code></li>
<li><strong>Repository 接口</strong>UseCase 通过 <code>domain/repositories/message_repository.dart</code> 接口调用</li>
<li><strong>Repository 实现</strong><code>data/repositories/message_repository_impl.dart</code> 实现具体逻辑</li>
<li><strong>本地优先</strong>:先保存到 <code>data/local/message_local_ds.dart</code></li>
<li><strong>网络发送</strong>Repository 直接调 SDKApiClient / SocketClient发送</li>
<li><strong>服务器确认</strong>WebSocket 服务器确认接收</li>
<li><strong>状态更新</strong>:更新本地数据库中的消息状态</li>
<li><strong>数据返回</strong>:层层返回,最终更新 UI</li>
</ol>
<h4>完整代码示例Riverpod 实现)</h4>
<p><strong>1. UI 层ConsumerWidget</strong></p>
<pre><code>// 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) =&gt; viewModel.sendMessage(content),
),
);
}
}
</code></pre>
<p><strong>2. Presentation 层StateNotifier</strong></p>
<pre><code>// features/chat/presentation/chat_view_model.dart
class ChatViewModel extends StateNotifier&lt;ChatState&gt; {
ChatViewModel(this._sendMessageUseCase) : super(const ChatState());
final SendMessageUseCase _sendMessageUseCase;
Future&lt;void&gt; sendMessage(String content) async {
state = state.copyWith(isLoading: true);
try {
await _sendMessageUseCase(content);
} finally {
state = state.copyWith(isLoading: false);
}
}
}
// Provider
final chatViewModelProvider =
StateNotifierProvider.autoDispose&lt;ChatViewModel, ChatState&gt;((ref) {
return ChatViewModel(ref.watch(sendMessageUseCaseProvider));
});
</code></pre>
<p><strong>3. Domain 层UseCase + Provider</strong></p>
<pre><code>// features/chat/usecases/send_message_usecase.dart
class SendMessageUseCase {
final MessageRepository _repository;
SendMessageUseCase(this._repository);
Future&lt;Message&gt; 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&lt;Message&gt; sendMessage(String content);
Future&lt;List&lt;Message&gt;&gt; getMessages(String chatId);
}
</code></pre>
<p><strong>4. Data 层Repository 实现 + Providers</strong></p>
<pre><code>// data/repositories/message_repository_impl.dart
class MessageRepositoryImpl implements MessageRepository {
final MessageLocalDataSource _localDS;
final ApiClient _client; // 直接注入 ApiClient / SocketClient
MessageRepositoryImpl(this._localDS, this._client);
@override
Future&lt;Message&gt; sendMessage(String content) async {
// 1. 先保存到本地
final localMessage = await _localDS.saveMessage(content, status: 'sending');
// 2. 直接调 SDK 发送到服务器
try {
final serverMessage = await _client.executeRequest(
SendMessageRequest(chatId: localMessage.chatId, content: content),
);
// 3. 更新本地状态
await _localDS.updateMessageStatus(localMessage.id, 'sent');
return serverMessage!.toEntity();
} catch (e) {
await _localDS.updateMessageStatus(localMessage.id, 'failed');
rethrow;
}
}
}
// Provider
@riverpod
MessageRepository messageRepository(MessageRepositoryRef ref) {
return MessageRepositoryImpl(
ref.watch(messageLocalDataSourceProvider),
ref.watch(apiClientProvider), // 直接注入 ApiClient
);
}
</code></pre>
<p><strong>5. Local DataSource + Provider</strong></p>
<pre><code>// data/local/message_local_ds.dart
class MessageLocalDataSource {
final AppDatabase _db;
Future&lt;MessageDTO&gt; saveMessage(String content, {required String status}) {
// Drift 操作
}
}
// Provider
@riverpod
MessageLocalDataSource messageLocalDataSource(MessageLocalDataSourceRef ref) {
return MessageLocalDataSourceImpl(ref.watch(databaseProvider));
}
</code></pre>
<h3 id="9-3-加载会话列表流程">9.3 加载会话列表流程</h3>
<div class="mermaid">
sequenceDiagram
participant UI as features/chat_list/view/<br/>chat_list_page.dart
participant VM as features/chat_list/presentation/<br/>chat_list_view_model.dart
participant UC as features/chat_list/usecases/<br/>load_chat_list_usecase.dart
participant Repo as domain/repositories/<br/>chat_repository.dart
participant RepoImpl as data/repositories/<br/>chat_repository_impl.dart
participant Cache as data/cache/<br/>cache_manager.dart
participant LocalDS as data/local/<br/>chat_local_ds.dart
participant SDK as networks_sdk/<br/>ApiClient
UI->>VM: 1. 页面初始化
VM->>UC: 2. 调用 LoadChatListUseCase
UC->>Repo: 3. 调用 Repository 接口
Repo->>RepoImpl: 4. Repository 实现
RepoImpl->>Cache: 5. 检查缓存
alt 缓存命中
Cache-->>RepoImpl: 6a. 返回缓存数据
else 缓存未命中
RepoImpl->>LocalDS: 6b. 读取本地数据库
LocalDS-->>RepoImpl: 7. 返回本地数据
RepoImpl->>SDK: 8. 直接调 ApiClient 请求远程数据
SDK-->>RepoImpl: 9. 返回最新数据
RepoImpl->>LocalDS: 10. 更新本地数据库
RepoImpl->>Cache: 11. 更新缓存
end
RepoImpl-->>UC: 12. 返回 Chat Entity 列表
UC-->>VM: 13. 返回结果
VM-->>UI: 14. 更新 UI 状态
UI->>UI: 15. 显示会话列表
Note over UI,SDK: 缓存优先策略:缓存 → 本地 → 远程
</div>
<h3 id="9-4-跨-feature-交互">9.4 跨 Feature 交互</h3>
<p>不同 Feature 之间通过共享的 Repository 接口交互:</p>
<div class="mermaid">
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
</div>
<p><strong>关键原则</strong></p>
<ul>
<li>Feature 之间不直接依赖</li>
<li>通过共享的 Repository 接口通信</li>
<li>Repository 实现在 Data 层统一管理</li>
<li>保持 Feature 的独立性和可测试性</li>
</ul>
<h3 id="9-5-数据同步策略">9.5 数据同步策略</h3>
<p>Repository 层负责协调本地和远程数据:</p>
<ul>
<li><strong>读取数据</strong>:缓存 → 本地数据库 → 远程服务器(三级缓存)</li>
<li><strong>写入数据</strong>:先写入本地,再同步到远程(本地优先)</li>
<li><strong>冲突解决</strong>:使用时间戳或版本号解决冲突</li>
<li><strong>离线支持</strong>:本地数据库支持离线操作,网络恢复后自动同步</li>
<li><strong>增量同步</strong>:只同步变更的数据,减少网络传输</li>
</ul>
<h3 id="9-6-层级依赖总结">9.6 层级依赖总结</h3>
<div class="mermaid">
flowchart TD
subgraph FeatureLayer[Feature Layer - 按页面垂直切片]
direction TB
FeatureView[features/*/view/<br/>页面 + 组件]
FeaturePresentation[features/*/presentation/<br/>ViewModel + 状态]
FeatureDomain[features/*/domain/<br/>UseCase + Entity]
end
subgraph GlobalDomain[Global Domain - 共享接口]
direction TB
RepoInterfaces[domain/repositories/<br/>Repository 接口]
ValueObjects[domain/value_objects/<br/>值对象]
end
subgraph DataLayer[Data Layer - 全局实现]
direction TB
RepoImpl[data/repositories/<br/>Repository 实现]
LocalDS2[data/local/<br/>本地数据源]
DTOs[data/models/<br/>DTO 模型]
end
subgraph CoreLayer[Core Layer - 主 App 内部]
subgraph Foundation[core/foundation/ - 应用级基础设施]
direction TB
AppInfra[Constants / Config / Errors<br/>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<br/>media_sdk / rtc_sdk / notification_sdk<br/>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
</div>
<blockquote>
<p><strong>架构核心</strong></p>
<p>1. <strong>Feature 垂直切片</strong>:每个 Feature 包含 UI → Presentation → Domain 的完整链路</p>
<p>2. <strong>全局 Repository 接口</strong>domain/repositories/ 定义数据访问接口</p>
<p>3. <strong>统一 Data 实现</strong>data/ 实现所有 Repository管理所有数据源</p>
<p>4. <strong>Core Foundation 支撑</strong>core/foundation/ 提供应用级基础设施SDK 能力由 packages/ 独立 Package 提供</p>
<p>5. <strong>L10n 国际化</strong>packages/l10n_sdk 提供翻译资源和语言切换,被 core/ui 和 Feature 层单向引用</p>
<p>6. <strong>Core UI 统一视觉</strong>core/ui/ 提供基础定义、基础组件和业务组合组件Feature 层统一复用</p>
<p>7. <strong>严格单向依赖</strong>core/ui/ → l10n_sdk → core/foundation/,任何层级不可反向依赖</p>
</blockquote>
<hr>
<hr>
<h2 id="part6-enterprise" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第六部分:企业级架构关键考虑因素</h2>
<p>在实际的企业级 IM 应用开发中,除了基础架构设计,还需要考虑以下关键因素,这些因素直接影响系统的可扩展性、性能、可维护性和长期收益。</p>
<h3 id="5-1-bridge-能力规划">5.1 跨平台交互 Bridge 能力</h3>
<p><strong>核心理念</strong>:提前规划与其他平台的交互能力,确保 IM App 可以嵌入到各种宿主环境(企业内部平台、第三方应用等)。</p>
<h4>Bridge 架构设计</h4>
<div class="mermaid">
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
</div>
<h4>Bridge 能力接口定义</h4>
<pre><code class="language-dart">/// Bridge 能力接口 - 抽象层
abstract class BridgeCapability {
/// 检测能力是否可用
Future&lt;bool&gt; isAvailable();
/// 初始化能力
Future&lt;void&gt; initialize();
/// 能力名称
String get name;
/// 能力版本
String get version;
}
/// Bridge 管理器 - 统一管理所有 Bridge 能力
class BridgeManager {
static final BridgeManager _instance = BridgeManager._internal();
factory BridgeManager() => _instance;
BridgeManager._internal();
final Map&lt;String, BridgeCapability&gt; _capabilities = {};
/// 注册能力
void registerCapability(BridgeCapability capability) {
_capabilities[capability.name] = capability;
}
/// 检测能力是否可用
Future&lt;bool&gt; hasCapability(String name) async {
final capability = _capabilities[name];
if (capability == null) return false;
return await capability.isAvailable();
}
/// 获取能力
T? getCapability&lt;T extends BridgeCapability&gt;(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&lt;bool&gt; isAvailable() async {
// 检测宿主环境是否支持网络请求
return true;
}
@override
Future&lt;void&gt; initialize() async {
// 初始化网络能力
}
/// 通过 Bridge 发送网络请求
Future&lt;Response&gt; request(String url, {
required HTTPMethod method,
Map&lt;String, dynamic&gt;? 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&lt;bool&gt; isAvailable() async {
return true;
}
@override
Future&lt;void&gt; initialize() async {}
/// 存储数据
Future&lt;void&gt; setItem(String key, String value) async {
await _callHost('storage.set', {'key': key, 'value': value});
}
/// 读取数据
Future&lt;String?&gt; getItem(String key) async {
return await _callHost('storage.get', {'key': key});
}
}
</code></pre>
<h4>能力检测与降级策略</h4>
<pre><code class="language-dart">/// 能力检测与降级
class BridgeCapabilityChecker {
/// 检测所有必需能力
static Future&lt;Map&lt;String, bool&gt;&gt; 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&lt;void&gt; 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');
}
}
}
</code></pre>
<div style="background: #fff3cd; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p><strong>关键价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>提前规划</strong>:在架构设计初期就考虑 Bridge 能力,避免后期重构</li>
<li><strong>能力检测</strong>:运行时动态检测宿主环境能力,自动适配</li>
<li><strong>优雅降级</strong>:能力不可用时自动降级,不影响核心功能</li>
<li><strong>统一接口</strong>:通过 Bridge 层统一调用,屏蔽平台差异</li>
</ul>
</div>
<h3 id="5-2-数据获取策略">5.2 数据获取多层策略</h3>
<p><strong>核心理念</strong>:通过内存缓存、热表、冷表、网络请求的多层数据获取策略,优化性能和用户体验。</p>
<h4>数据分层架构</h4>
<div class="mermaid">
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
</div>
<h4>数据层定义</h4>
<table>
<thead>
<tr>
<th>层级</th>
<th>存储位置</th>
<th>数据特点</th>
<th>访问速度</th>
<th>容量</th>
<th>生命周期</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>L1: 内存缓存</strong></td>
<td>RAM</td>
<td>最近访问、高频访问</td>
<td>极快(&lt;1ms</td>
<td>100MB</td>
<td>应用运行期间</td>
</tr>
<tr>
<td><strong>L2: 热表</strong></td>
<td>Drift + 索引</td>
<td>频繁访问、近期活跃</td>
<td>1-10ms</td>
<td>1GB</td>
<td>30 天</td>
</tr>
<tr>
<td><strong>L3: 冷表</strong></td>
<td>Drift</td>
<td>历史数据、低频访问</td>
<td>10-50ms</td>
<td>10GB</td>
<td>永久</td>
</tr>
<tr>
<td><strong>L4: 网络</strong></td>
<td>远程服务器</td>
<td>最新数据、全量数据</td>
<td>100-1000ms</td>
<td>无限</td>
<td>永久</td>
</tr>
</tbody>
</table>
<h4>数据获取策略实现</h4>
<pre><code class="language-dart">/// 多层数据获取策略
class DataFetchStrategy&lt;T&gt; {
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&lt;T?&gt; fetch(String key) async {
// L1: 检查内存缓存
final cached = memoryCache.get&lt;T&gt;(key);
if (cached != null) {
_recordHit('memory', key);
return cached;
}
// L2: 检查热表
final hot = await hotTable.query&lt;T&gt;(key);
if (hot != null) {
_recordHit('hot_table', key);
memoryCache.set(key, hot); // 回填内存
return hot;
}
// L3: 检查冷表
final cold = await coldTable.query&lt;T&gt;(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&lt;T&gt;(key);
if (data != null) {
_recordHit('network', key);
// 同步到各层缓存
await _syncToCache(key, data);
return data;
}
} catch (e) {
_recordError('network', key, e);
}
return null;
}
/// 同步数据到缓存
Future&lt;void&gt; _syncToCache(String key, T data) async {
// 写入内存
memoryCache.set(key, data);
// 写入热表
await hotTable.insert(key, data);
// 写入冷表(持久化)
await coldTable.insert(key, data);
}
/// 判断是否应该提升到热表
Future&lt;bool&gt; _shouldPromoteToHot(String key) async {
final accessCount = await _getAccessCount(key);
return accessCount &gt; 5; // 访问超过 5 次提升到热表
}
}
/// 内存缓存LRU
class MemoryCache {
final int maxSize;
final Map&lt;String, dynamic&gt; _cache = {};
final List&lt;String&gt; _accessOrder = [];
MemoryCache({this.maxSize = 1000});
T? get&lt;T&gt;(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&lt;T?&gt; query&lt;T&gt;(String key) async {
// SELECT * FROM hot_table WHERE key = ? AND last_access &gt; (NOW() - 30 days)
return await _database.query('hot_table', where: 'key = ?', whereArgs: [key]);
}
/// 插入热表
Future&lt;void&gt; insert(String key, dynamic value) async {
await _database.insert('hot_table', {
'key': key,
'value': jsonEncode(value),
'last_access': DateTime.now().toIso8601String(),
});
}
}
/// 冷表数据源(历史数据)
class ColdTableDataSource {
/// 查询冷表
Future&lt;T?&gt; query&lt;T&gt;(String key) async {
// SELECT * FROM cold_table WHERE key = ?
return await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
}
/// 插入冷表
Future&lt;void&gt; insert(String key, dynamic value) async {
await _database.insert('cold_table', {
'key': key,
'value': jsonEncode(value),
'created_at': DateTime.now().toIso8601String(),
});
}
}
</code></pre>
<h4>数据迁移策略</h4>
<pre><code class="language-dart">/// 数据迁移服务 - 热表与冷表之间的数据流动
class DataMigrationService {
/// 定期清理过期的热表数据
Future&lt;void&gt; cleanupHotTable() async {
// 删除 30 天未访问的数据
await _database.delete(
'hot_table',
where: 'last_access &lt; ?',
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
);
}
/// 将热数据迁移到冷表
Future&lt;void&gt; migrateHotToCold() async {
final oldData = await _database.query(
'hot_table',
where: 'last_access &lt; ?',
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
);
for (final row in oldData) {
await _database.insert('cold_table', row);
}
}
/// 提升冷数据到热表
Future&lt;void&gt; promoteColdToHot(String key) async {
final data = await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
if (data != null) {
await _database.insert('hot_table', data);
}
}
}
</code></pre>
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #388e3c; margin: 20px 0;">
<p><strong>性能收益</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>响应速度</strong>:内存缓存命中率高,响应时间极快</li>
<li><strong>网络流量</strong>:大幅减少网络请求,节省流量和电量</li>
<li><strong>离线可用</strong>:冷表保存历史数据,离线时仍可访问</li>
<li><strong>自动优化</strong>:根据访问频率自动调整数据存储位置</li>
</ul>
</div>
<h3 id="5-3-中间层设计">5.3 大量中间层的价值</h3>
<p><strong>核心理念</strong>:通过大量的中间层(抽象层、适配层、转换层),实现高度解耦、易于测试、便于替换。</p>
<h4>中间层架构</h4>
<div class="mermaid">
flowchart TD
UI[UI Layer] --> ViewModelAdapter[ViewModel 适配层]
ViewModelAdapter --> UseCase[UseCase 抽象层]
UseCase --> RepositoryInterface[Repository 接口层]
RepositoryInterface --> RepositoryImpl[Repository 实现层]
RepositoryImpl --> NetworkSDK[Network SDK<br/>packages/networks_sdk]
RepositoryImpl --> StorageSDK[Storage SDK<br/>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
</div>
<h4>中间层的类型与作用</h4>
<table>
<thead>
<tr>
<th>中间层类型</th>
<th>位置</th>
<th>作用</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>抽象层</strong></td>
<td>Domain</td>
<td>定义业务接口,隔离实现细节</td>
<td>Repository 接口、UseCase 抽象类</td>
</tr>
<tr>
<td><strong>适配层</strong></td>
<td>Data</td>
<td>适配不同数据源,统一接口</td>
<td>LocalDataSource、CacheManager</td>
</tr>
<tr>
<td><strong>转换层</strong></td>
<td>Data</td>
<td>DTO ↔ Entity 数据转换</td>
<td>Mapper、Converter</td>
</tr>
<tr>
<td><strong>协议层</strong></td>
<td>Core</td>
<td>封装通信协议,屏蔽底层细节</td>
<td>APIRequestable、WebSocketProtocol</td>
</tr>
<tr>
<td><strong>策略层</strong></td>
<td>Core/Data</td>
<td>封装算法和策略,易于替换</td>
<td>CacheStrategy、RetryStrategy</td>
</tr>
</tbody>
</table>
<h4>中间层实践示例</h4>
<pre><code class="language-dart">// 1. Repository 接口层(抽象层)
abstract class ChatRepository {
Future&lt;List&lt;Message&gt;&gt; getMessages(String chatId);
Future&lt;void&gt; sendMessage(Message message);
}
// 2. Repository 实现层(直接注入 ApiClient
class ChatRepositoryImpl implements ChatRepository {
final ApiClient client;
final LocalDataSource localDataSource;
final MessageMapper mapper;
ChatRepositoryImpl({
required this.client,
required this.localDataSource,
required this.mapper,
});
@override
Future&lt;List&lt;Message&gt;&gt; getMessages(String chatId) async {
// 先从本地获取
final localDTOs = await localDataSource.getMessages(chatId);
if (localDTOs.isNotEmpty) {
return localDTOs.map(mapper.toEntity).toList();
}
// 直接调 ApiClient 从远程获取
final response = await client.executeRequest(
GetMessagesRequest(chatId: chatId),
);
// 保存到本地
await localDataSource.saveMessages(response?.messages ?? []);
// 转换为 Entity
return (response?.messages ?? []).map(mapper.toEntity).toList();
}
}
// 4. 转换层Mapper
class MessageMapper {
Message toEntity(MessageDTO dto) {
return Message(
id: dto.id,
content: dto.content,
senderId: dto.senderId,
timestamp: DateTime.parse(dto.timestamp),
);
}
MessageDTO toDTO(Message entity) {
return MessageDTO(
id: entity.id,
content: entity.content,
senderId: entity.senderId,
timestamp: entity.timestamp.toIso8601String(),
);
}
}
// 5. 策略层
abstract class CacheStrategy {
bool shouldCache(String key);
Duration getCacheDuration(String key);
}
class MessageCacheStrategy implements CacheStrategy {
@override
bool shouldCache(String key) {
// 最近 100 条消息缓存
return true;
}
@override
Duration getCacheDuration(String key) {
return Duration(hours: 24);
}
}
</code></pre>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #2196f3; margin: 20px 0;">
<p><strong>中间层的价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>解耦</strong>:各层通过接口通信,修改实现不影响调用方</li>
<li><strong>可测试</strong>:每层可独立测试,易于 Mock</li>
<li><strong>可替换</strong>:底层实现可随时替换(如换数据库、换 API</li>
<li><strong>可扩展</strong>:新增功能只需新增实现,不修改接口</li>
<li><strong>可维护</strong>:职责清晰,问题定位快速</li>
</ul>
</div>
<h3 id="5-4-系统能力划分">5.4 系统能力划分</h3>
<p><strong>核心理念</strong>:将系统能力划分为基础能力、业务能力、快速响应机制、部署策略,实现高内聚低耦合。</p>
<h4>能力划分架构</h4>
<div class="mermaid">
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
</div>
<h4>基础能力层</h4>
<p><strong>定义</strong>:与业务无关的通用技术能力,可复用到任何项目。</p>
<table>
<thead>
<tr>
<th>能力</th>
<th>说明</th>
<th>SDK</th>
<th>可复用性</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>网络通信</strong></td>
<td>HTTP/WebSocket/gRPC</td>
<td>NetworkSDK</td>
<td>完全通用</td>
</tr>
<tr>
<td><strong>数据存储</strong></td>
<td>Drift/SharedPreferences/SecureStorage</td>
<td>StorageSDK</td>
<td>完全通用</td>
</tr>
<tr>
<td><strong>端对端加密</strong></td>
<td>RSA/AES 双层加密 + Native 密钥同步</td>
<td>CipherGuardSDK</td>
<td>完全通用</td>
</tr>
<tr>
<td><strong>媒体处理</strong></td>
<td>图片/视频/音频压缩</td>
<td>MediaSDK</td>
<td>高度通用</td>
</tr>
<tr>
<td><strong>音视频通话</strong></td>
<td>WebRTC/Agora</td>
<td>RTCSDK</td>
<td>较为通用</td>
</tr>
<tr>
<td><strong>推送通知</strong></td>
<td>FCM/APNs/本地通知</td>
<td>NotificationSDK</td>
<td>高度通用</td>
</tr>
<tr>
<td><strong>推送解密</strong></td>
<td>iOS App Group 密钥同步Notification Extension</td>
<td>CipherGuardSDK</td>
<td>高度通用</td>
</tr>
<tr>
<td><strong>协议序列化</strong></td>
<td>Protocol Buffers/JSON</td>
<td>ProtocolSDK</td>
<td>完全通用</td>
</tr>
</tbody>
</table>
<h4>业务能力层</h4>
<p><strong>定义</strong>IM 领域特定的业务能力,基于基础能力层构建。</p>
<table>
<thead>
<tr>
<th>能力</th>
<th>说明</th>
<th>依赖基础能力</th>
<th>可复用性</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>聊天功能</strong></td>
<td>单聊/群聊/消息管理</td>
<td>Network + Storage + Protocol</td>
<td>IM 领域高度复用</td>
</tr>
<tr>
<td><strong>联系人管理</strong></td>
<td>好友/黑名单/通讯录</td>
<td>Network + Storage</td>
<td>IM 领域高度复用</td>
</tr>
<tr>
<td><strong>群组管理</strong></td>
<td>创建群/成员管理/权限</td>
<td>Network + Storage</td>
<td>IM 领域高度复用</td>
</tr>
<tr>
<td><strong>音视频通话</strong></td>
<td>一对一/多人通话</td>
<td>RTC + Network</td>
<td>IM 领域较为复用</td>
</tr>
<tr>
<td><strong>消息推送</strong></td>
<td>离线推送/在线推送</td>
<td>Notification + Network</td>
<td>IM 领域高度复用</td>
</tr>
</tbody>
</table>
<h4>快速响应机制</h4>
<p><strong>目标</strong>:通过智能缓存、预加载、性能优化,实现极速响应用户操作。</p>
<pre><code class="language-dart">/// 快速响应管理器
class FastResponseManager {
/// 智能缓存 - 预测用户行为并缓存
Future&lt;void&gt; 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&lt;void&gt; preload() async {
// 预加载用户资料
await UserProfilePreloader.preload();
// 预加载表情包
await EmojiPreloader.preload();
// 预加载常用设置
await SettingsPreloader.preload();
}
/// 性能优化 - 优化关键路径
Future&lt;void&gt; 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 &gt; 100) {
Logger.warn('Slow operation: $operation took ${ms}ms');
}
// 上报性能数据
Analytics.track('response_time', {
'operation': operation,
'duration_ms': ms,
});
}
}
</code></pre>
<h4>容器化部署?</h4>
<p><strong>潜在方向</strong>:可考虑通过容器化实现快速部署、弹性扩展、版本管理。</p>
<ul>
<li><strong>Docker 容器</strong>:应用容器化打包</li>
<li><strong>Kubernetes 编排</strong>:容器编排和管理</li>
<li><strong>CI/CD 流水线</strong>:自动化部署流程</li>
<li><strong>弹性扩展</strong>:根据负载自动扩容缩容</li>
</ul>
<p><em>注:容器化部署更多适用于后端服务,对于移动端 App 的应用场景需要进一步评估。</em></p>
<div style="background: #f3e5f5; padding: 20px; border-radius: 8px; border-left: 4px solid #7b1fa2; margin: 20px 0;">
<p><strong>系统能力划分的价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>清晰边界</strong>:基础能力与业务能力分离,职责明确</li>
<li><strong>高复用性</strong>:基础能力可复用到其他项目</li>
<li><strong>快速响应</strong>:智能缓存和预加载提升用户体验</li>
<li><strong>灵活部署</strong>:支持多种部署方式,适应不同场景</li>
<li><strong>易于维护</strong>:能力独立开发和测试</li>
</ul>
</div>
<h3 id="5-5-代码-review-机制">5.5 严格的 Code Review 机制</h3>
<p><strong>核心理念</strong>:通过严格的代码审查机制,保证代码质量、一致性和可维护性。</p>
<h4>Code Review 流程</h4>
<div class="mermaid">
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
</div>
<h4>Code Review 检查清单</h4>
<table>
<thead>
<tr>
<th>类别</th>
<th>检查项</th>
<th>重要性</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>架构合规</strong></td>
<td>是否遵循分层架构?是否违反依赖规则?</td>
<td>必须</td>
</tr>
<tr>
<td><strong>代码规范</strong></td>
<td>是否符合命名规范?是否通过 Lint 检查?</td>
<td>必须</td>
</tr>
<tr>
<td><strong>设计原则</strong></td>
<td>是否符合 SOLID 原则?是否高内聚低耦合?</td>
<td>必须</td>
</tr>
<tr>
<td><strong>测试覆盖</strong></td>
<td>是否编写单元测试?测试覆盖率是否充分?</td>
<td>必须</td>
</tr>
<tr>
<td><strong>性能</strong></td>
<td>是否有性能问题?是否有内存泄漏?</td>
<td>重要</td>
</tr>
<tr>
<td><strong>安全性</strong></td>
<td>是否有安全漏洞?敏感数据是否加密?</td>
<td>必须</td>
</tr>
<tr>
<td><strong>可读性</strong></td>
<td>代码是否易于理解?是否有必要的注释?</td>
<td>重要</td>
</tr>
<tr>
<td><strong>可维护性</strong></td>
<td>是否易于修改?是否有重复代码?</td>
<td>重要</td>
</tr>
</tbody>
</table>
<h4>自动化检查工具</h4>
<pre><code class="language-yaml"># 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></pre>
<h4>Code Review 最佳实践</h4>
<pre><code class="language-dart">/// Code Review 检查工具
class CodeReviewChecker {
/// 架构合规检查
static List&lt;String&gt; checkArchitectureCompliance(String filePath) {
final issues = &lt;String&gt;[];
// 检查分层依赖
if (_hasReverseDependency(filePath)) {
issues.add('发现反向依赖Domain 层不能依赖 Data 层');
}
// 检查跨层调用
if (_hasCrossLayerCall(filePath)) {
issues.add('发现跨层调用UI 层不能直接调用 Repository');
}
return issues;
}
/// 代码质量检查
static List&lt;String&gt; checkCodeQuality(String filePath) {
final issues = &lt;String&gt;[];
// 检查类复杂度
if (_getClassComplexity(filePath) &gt; 10) {
issues.add('类复杂度过高,建议拆分');
}
// 检查方法长度
if (_getMaxMethodLength(filePath) &gt; 50) {
issues.add('方法过长,建议拆分');
}
// 检查重复代码
if (_hasDuplicateCode(filePath)) {
issues.add('发现重复代码,建议抽取');
}
return issues;
}
}
</code></pre>
<div style="background: #fff3cd; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107; margin: 20px 0;">
<p><strong>Code Review 的价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>提前发现问题</strong>:在代码合并前发现 Bug 和设计问题</li>
<li><strong>保证质量</strong>:确保代码符合架构规范和编码标准</li>
<li><strong>知识共享</strong>:团队成员相互学习和成长</li>
<li><strong>统一风格</strong>:保持代码库的一致性</li>
<li><strong>降低维护成本</strong>:高质量代码更易于维护</li>
</ul>
</div>
<h3 id="5-6-长期收益">5.6 长期收益分析</h3>
<p><strong>核心理念</strong>:架构设计不是为了短期收益,而是为了长期可持续发展。</p>
<h4>短期 vs 长期对比</h4>
<table>
<thead>
<tr>
<th>维度</th>
<th>短期方案</th>
<th>长期方案(本架构)</th>
<th>长期对比</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>初期开发</strong></td>
<td>快速</td>
<td>需要更多时间</td>
<td>-</td>
</tr>
<tr>
<td><strong>维护成本</strong></td>
<td>高(大量时间修 Bug</td>
<td>低(少量维护)</td>
<td>大幅降低</td>
</tr>
<tr>
<td><strong>Bug 率</strong></td>
<td>频繁出现 Bug</td>
<td>Bug 很少</td>
<td>显著减少</td>
</tr>
<tr>
<td><strong>新功能开发</strong></td>
<td>缓慢</td>
<td>快速</td>
<td>明显提速</td>
</tr>
<tr>
<td><strong>技术债务</strong></td>
<td>严重</td>
<td>很少</td>
<td>避免重构</td>
</tr>
<tr>
<td><strong>团队效率</strong></td>
<td>低(大量救火)</td>
<td>高(专注开发)</td>
<td>显著提升</td>
</tr>
<tr>
<td><strong>代码质量</strong></td>
<td>差(难以维护)</td>
<td>优(易于维护)</td>
<td>可持续发展</td>
</tr>
</tbody>
</table>
<h4>长期收益体现</h4>
<p><strong>初期投入 vs 长期回报</strong></p>
<ul>
<li><strong>初期阶段</strong>:需要更多时间进行架构设计和基础设施建设</li>
<li><strong>维护阶段</strong>:维护成本大幅降低,团队可以专注新功能开发</li>
<li><strong>迭代阶段</strong>:新功能开发速度显著提升,架构优势逐渐显现</li>
<li><strong>长期发展</strong>:技术债务少,代码库健康,可持续发展</li>
</ul>
<p><strong>收益来源</strong></p>
<ul>
<li><strong>维护成本降低</strong>:良好的架构设计减少维护工作量</li>
<li><strong>Bug 率降低</strong>:严格的分层和测试机制减少 Bug 数量</li>
<li><strong>开发效率提升</strong>:清晰的架构和丰富的基础组件加速开发</li>
<li><strong>避免重构</strong>:提前规划避免后期大规模重构</li>
</ul>
<h4>长期收益的关键指标</h4>
<div class="mermaid">
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
</div>
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 8px; margin: 20px 0;">
<h4 style="margin-top: 0; color: white;">长期收益总结</h4>
<ul style="margin-bottom: 0;">
<li><strong>投资回报</strong>:初期投入更多时间,长期回报远超初期投入</li>
<li><strong>维护成本</strong>:大幅降低,团队可以专注创新而非救火</li>
<li><strong>Bug 率</strong>:显著减少,用户体验持续提升</li>
<li><strong>开发效率</strong>:新功能开发明显提速,快速响应市场</li>
<li><strong>技术债务</strong>:避免大规模重构,保持代码库健康</li>
<li><strong>团队成长</strong>:规范的架构让团队成员快速成长</li>
<li><strong>竞争优势</strong>:更快的迭代速度,更高的产品质量</li>
</ul>
</div>
<h3 id="5-7-日志与监控系统">5.7 日志与监控系统</h3>
<p><strong>核心理念</strong>:通过完善的日志系统和运行监控,实现问题快速定位、性能实时追踪、用户行为分析。</p>
<h4>5.7.1 日志系统设计</h4>
<p><strong>日志分级策略</strong></p>
<table>
<thead>
<tr>
<th>级别</th>
<th>用途</th>
<th>输出位置</th>
<th>保留时间</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Debug</strong></td>
<td>开发调试信息</td>
<td>仅开发环境控制台</td>
<td>不保存</td>
</tr>
<tr>
<td><strong>Info</strong></td>
<td>正常运行信息、关键操作</td>
<td>本地文件</td>
<td>7 天</td>
</tr>
<tr>
<td><strong>Warning</strong></td>
<td>潜在问题、异常情况</td>
<td>本地文件 + 远程上报</td>
<td>30 天</td>
</tr>
<tr>
<td><strong>Error</strong></td>
<td>错误信息、异常堆栈</td>
<td>本地文件 + 远程立即上报</td>
<td>90 天</td>
</tr>
<tr>
<td><strong>Fatal</strong></td>
<td>严重错误、崩溃</td>
<td>本地文件 + 远程立即上报 + 告警</td>
<td>永久</td>
</tr>
</tbody>
</table>
<p><strong>日志分类</strong></p>
<div class="mermaid">
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
</div>
<h4>5.7.2 日志系统实现</h4>
<pre><code class="language-dart">/// 日志系统 - 统一日志管理
class LoggerService {
static final LoggerService _instance = LoggerService._internal();
factory LoggerService() => _instance;
LoggerService._internal();
/// 日志级别
void debug(String message, {Map&lt;String, dynamic&gt;? data}) {
if (!kReleaseMode) {
_log(LogLevel.debug, message, data);
}
}
void info(String message, {Map&lt;String, dynamic&gt;? data}) {
_log(LogLevel.info, message, data);
_saveToLocal(LogLevel.info, message, data);
}
void warning(String message, {Map&lt;String, dynamic&gt;? data}) {
_log(LogLevel.warning, message, data);
_saveToLocal(LogLevel.warning, message, data);
_uploadToRemote(LogLevel.warning, message, data);
}
void error(String message, {
Map&lt;String, dynamic&gt;? 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&lt;String, dynamic&gt;? 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&lt;String, dynamic&gt;? headers,
Map&lt;String, dynamic&gt;? 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 &gt;= 400 ? LogLevel.warning : LogLevel.info;
_log(level, 'Network Response', {
'url': url,
'statusCode': statusCode,
'duration': '${duration.inMilliseconds}ms',
'error': error,
});
}
/// 用户操作日志
void logUserAction(String action, {Map&lt;String, dynamic&gt;? data}) {
info('User Action: $action', data: data);
}
/// 性能日志
void logPerformance(String operation, Duration duration) {
if (duration.inMilliseconds &gt; 100) {
warning('Slow Operation: $operation', data: {
'duration': '${duration.inMilliseconds}ms',
});
}
}
/// 隐私数据脱敏
Map&lt;String, dynamic&gt;? _sanitizeData(Map&lt;String, dynamic&gt;? data) {
if (data == null) return null;
final sanitized = Map&lt;String, dynamic&gt;.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 }
</code></pre>
<h4>5.7.3 运行监控系统</h4>
<p><strong>监控维度</strong></p>
<table>
<thead>
<tr>
<th>监控类型</th>
<th>监控指标</th>
<th>告警阈值</th>
<th>采集频率</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>性能监控</strong></td>
<td>CPU 占用率、内存占用、帧率FPS</td>
<td>内存 &gt; 500MB<br/>FPS &lt; 50</td>
<td>实时</td>
</tr>
<tr>
<td><strong>错误监控</strong></td>
<td>Crash 率、ANR 率、异常捕获</td>
<td>Crash 率 &gt; 0.1%</td>
<td>实时</td>
</tr>
<tr>
<td><strong>网络监控</strong></td>
<td>请求成功率、响应时间、流量消耗</td>
<td>成功率 &lt; 95%<br/>响应时间 &gt; 3s</td>
<td>每次请求</td>
</tr>
<tr>
<td><strong>业务监控</strong></td>
<td>消息发送成功率、登录成功率</td>
<td>成功率 &lt; 98%</td>
<td>每次操作</td>
</tr>
<tr>
<td><strong>用户行为</strong></td>
<td>页面访问路径、功能使用频率</td>
<td>-</td>
<td>每次操作</td>
</tr>
</tbody>
</table>
<p><strong>监控系统架构</strong></p>
<div class="mermaid">
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
</div>
<h4>5.7.4 监控系统实现</h4>
<pre><code class="language-dart">/// 监控服务
class MonitorService {
static final MonitorService _instance = MonitorService._internal();
factory MonitorService() => _instance;
MonitorService._internal();
/// 性能监控
void trackPerformance() {
// 监控内存占用
final memoryUsage = _getMemoryUsage();
if (memoryUsage &gt; 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 &lt; 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 &gt;= 200 &amp;&amp; statusCode &lt; 300,
'error': error,
});
// 慢请求告警
if (duration.inSeconds &gt; 3) {
LoggerService().warning('Slow Network Request', data: {
'url': url,
'duration': '${duration.inSeconds}s',
});
}
// 请求失败告警
if (statusCode &gt;= 400) {
LoggerService().error('Network Request Failed', data: {
'url': url,
'statusCode': statusCode,
'error': error,
});
}
}
/// 业务监控
void trackBusinessEvent(String event, {
bool success = true,
Map&lt;String, dynamic&gt;? data,
}) {
_recordMetric('business.$event', {
'success': success,
...?data,
});
if (!success) {
LoggerService().warning('Business Event Failed: $event', data: data);
}
}
/// 用户行为监控
void trackUserBehavior(String page, String action, {Map&lt;String, dynamic&gt;? data}) {
_recordMetric('behavior', {
'page': page,
'action': action,
'timestamp': DateTime.now().toIso8601String(),
...?data,
});
}
/// 记录指标
void _recordMetric(String metric, Map&lt;String, dynamic&gt; data) {
// 本地缓存
_cacheMetric(metric, data);
// 批量上报(每 1 分钟或累计 100 条)
_scheduleUpload();
}
/// 批量上报
Future&lt;void&gt; _scheduleUpload() async {
// 实现批量上报逻辑
}
}
/// 使用示例
class ChatViewModel extends StateNotifier&lt;ChatState&gt; {
Future&lt;void&gt; 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;
}
}
}
</code></pre>
<h4>5.7.5 日志与监控最佳实践</h4>
<table>
<thead>
<tr>
<th>实践</th>
<th>说明</th>
<th>价值</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>结构化日志</strong></td>
<td>使用 JSON 格式记录日志,便于检索和分析</td>
<td>快速定位问题</td>
</tr>
<tr>
<td><strong>隐私保护</strong></td>
<td>敏感数据脱敏密码、Token、手机号</td>
<td>符合隐私合规</td>
</tr>
<tr>
<td><strong>批量上报</strong></td>
<td>本地缓存,定时批量上报,减少网络请求</td>
<td>节省流量和电量</td>
</tr>
<tr>
<td><strong>实时告警</strong></td>
<td>Fatal 级别错误立即上报并触发告警</td>
<td>及时发现严重问题</td>
</tr>
<tr>
<td><strong>性能优化</strong></td>
<td>日志写入异步化,不阻塞主线程</td>
<td>不影响用户体验</td>
</tr>
<tr>
<td><strong>存储管理</strong></td>
<td>本地日志定期清理,避免占用过多空间</td>
<td>节省存储空间</td>
</tr>
</tbody>
</table>
<div style="background: #e1f5ff; padding: 20px; border-radius: 8px; border-left: 4px solid #2196f3; margin: 20px 0;">
<p><strong>日志与监控的核心价值</strong></p>
<ul style="margin-bottom: 0;">
<li><strong>问题定位</strong>:通过完整的日志链路,快速定位问题根因</li>
<li><strong>性能优化</strong>:实时监控性能指标,及时发现和解决性能瓶颈</li>
<li><strong>用户体验</strong>:监控用户行为,优化产品功能和交互</li>
<li><strong>故障预警</strong>:实时告警机制,在问题扩散前及时处理</li>
<li><strong>数据驱动</strong>:基于监控数据做决策,而非主观猜测</li>
</ul>
</div>
<h2 id="part7-summary" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px;">第七部分:总结</h2>
<h3>架构优势</h3>
<ul>
<li><strong>Feature 驱动的垂直切片</strong>:每个功能页面独立成模块,包含完整的 UI → Presentation → Domain 链路</li>
<li><strong>清晰的职责划分</strong>四层架构Feature/Domain/Data/Core每层有明确的职责边界</li>
<li><strong>高度可测试</strong>每层可独立测试Feature 可独立验证</li>
<li><strong>易于维护</strong>:模块化设计,修改影响范围小,功能高度内聚</li>
<li><strong>可扩展性强</strong>:添加新 Feature 不影响现有代码,遵循标准模板</li>
<li><strong>技术栈独立</strong>:底层实现可随时替换,业务逻辑不受影响</li>
<li><strong>团队协作友好</strong>Feature 驱动便于并行开发,减少代码冲突</li>
<li><strong>低耦合高内聚</strong>Feature 之间通过 Repository 接口解耦</li>
</ul>
<h3>核心架构设计</h3>
<div class="mermaid">
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
</div>
<h3>最佳实践</h3>
<ol>
<li><strong>严格遵守分层依赖规则</strong>:单向向下依赖,禁止反向依赖和跨层调用</li>
<li><strong>Feature 垂直切片</strong>:每个 Feature 包含 view/presentation/domain 的完整链路</li>
<li><strong>全局 Repository 接口</strong>:所有 Repository 接口定义在 domain/repositories/</li>
<li><strong>统一 Data 实现</strong>:所有 Repository 实现在 data/repositories/</li>
<li><strong>UseCase 单一职责</strong>:每个 UseCase 只处理一个业务场景</li>
<li><strong>Riverpod 状态管理</strong>:使用 StateNotifier 管理 UI 状态Providers 管理依赖</li>
<li><strong>使用代码生成</strong>:利用 riverpod_generator 和 freezed 减少样板代码</li>
<li><strong>使用 Melos 管理依赖</strong>Mono-Repo 保证版本一致性</li>
<li><strong>编写完整测试</strong>:单元测试、集成测试、端到端测试</li>
<li><strong>UI 层使用 ConsumerWidget</strong>:通过 ref.watch 监听状态ref.read 读取 Provider</li>
</ol>
<h3>关键原则</h3>
<blockquote>
<p><strong>Feature 驱动</strong>:以页面为单位组织代码,每个 Feature 是完整的垂直切片</p>
<p><strong>依赖倒置</strong>高层模块不依赖低层模块两者都依赖抽象Repository 接口)</p>
<p><strong>单一职责</strong>每个模块只做一件事UseCase/ViewModel/Repository 各司其职</p>
</blockquote>
</main>
</div>
<script>
// 导航栏自动高亮当前章节
document.addEventListener('DOMContentLoaded', function() {
const navLinks = document.querySelectorAll('#sidebar-nav a');
const sections = [];
// 收集所有章节元素
navLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('#')) {
const id = href.substring(1);
const element = document.getElementById(id);
if (element) {
sections.push({
id: id,
element: element,
link: link
});
}
}
});
// 监听滚动事件,高亮当前章节
function highlightCurrentSection() {
// 用视口高度的 30% 作为触发线:章节标题进入顶部 30% 即视为"当前章节"
const triggerLine = window.scrollY + window.innerHeight * 0.3;
let currentSection = null;
// 找到当前滚动位置对应的章节(取最后一个 offsetTop <= 触发线的章节)
for (let i = sections.length - 1; i >= 0; i--) {
if (sections[i].element.offsetTop <= triggerLine) {
currentSection = sections[i];
break;
}
}
// 移除所有高亮
navLinks.forEach(link => link.classList.remove('active'));
// 高亮当前章节
if (currentSection) {
currentSection.link.classList.add('active');
}
}
// 节流函数,优化性能
let ticking = false;
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(function() {
highlightCurrentSection();
ticking = false;
});
ticking = true;
}
});
// 初始化高亮
highlightCurrentSection();
// 点击导航链接时平滑滚动并高亮
navLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const href = this.getAttribute('href');
if (href && href.startsWith('#')) {
const id = href.substring(1);
const element = document.getElementById(id);
if (element) {
// 平滑滚动到目标位置
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// 更新 URL不触发页面跳转
history.pushState(null, null, href);
// 立即高亮当前链接
navLinks.forEach(l => l.classList.remove('active'));
this.classList.add('active');
}
}
});
});
});
</script>
</body>
</html>