9866 lines
349 KiB
HTML
9866 lines
349 KiB
HTML
<!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">AppTab:Tab 如何切换</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 替换为刚才生成的 token,YOUR_USERNAME 替换为你的 Gitea 用户名
|
||
git clone https://YOUR_USERNAME:YOUR_TOKEN@gitea.winwayinfo.com/CUS-IM/customer-im-client.git
|
||
|
||
# 或者先 clone 再配置凭据
|
||
git clone https://gitea.winwayinfo.com/CUS-IM/customer-im-client.git
|
||
# 输入用户名和 token(token 作为密码)
|
||
</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 Silicon(M 系列)必须下载 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 二进制是 arm64(Apple Silicon 机器)
|
||
file $(which dart) # 应输出 Mach-O 64-bit executable arm64
|
||
</code></pre>
|
||
</li>
|
||
</ol>
|
||
|
||
<h4>第二步:安装 Homebrew + Ruby + CocoaPods(iOS / 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 本身写入 PATH(Apple Silicon M 芯片路径)
|
||
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
|
||
source ~/.zshrc
|
||
|
||
# 3. 验证 brew 可用
|
||
brew --version
|
||
|
||
# 4. 安装 Ruby 3.3(固定版本,不能用 brew install ruby 因为会装 4.x)
|
||
brew install ruby@3.3
|
||
|
||
# 5. 将 Ruby 3.3 写入 PATH(必须在系统 Ruby 2.6 之前)
|
||
echo 'export PATH="/opt/homebrew/opt/ruby@3.3/bin:$PATH"' >> ~/.zshrc
|
||
source ~/.zshrc
|
||
|
||
# 6. 验证 Ruby 版本(确认是 3.3.x)
|
||
ruby --version
|
||
|
||
# 7. 安装 CocoaPods
|
||
gem install cocoapods
|
||
|
||
# 8. pod 可执行文件路径写入 PATH
|
||
# gem 路径固定为 3.3.0
|
||
echo 'export PATH="/opt/homebrew/lib/ruby/gems/3.3.0/bin:$PATH"' >> ~/.zshrc
|
||
|
||
# 9. 终端编码设置(避免 pod 报 UTF-8 警告)
|
||
echo 'export LANG=en_US.UTF-8' >> ~/.zshrc
|
||
|
||
source ~/.zshrc
|
||
|
||
# 11. 验证
|
||
pod --version
|
||
</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_CONF,pod install 时读取该配置
|
||
cat > /tmp/openssl_pod.cnf << 'OPENSSL_EOF'
|
||
openssl_conf = openssl_init
|
||
[openssl_init]
|
||
ssl_conf = ssl_sect
|
||
[ssl_sect]
|
||
system_default = system_default_sect
|
||
[system_default_sect]
|
||
MinProtocol = TLSv1.2
|
||
CipherString = DEFAULT:@SECLEVEL=1
|
||
OPENSSL_EOF
|
||
export OPENSSL_CONF=/tmp/openssl_pod.cnf
|
||
</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.6,CocoaPods 要求 >= 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>(补充)安装 Xcode(iOS / 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>Settings(Windows/Linux)</code> 或 <code>Preferences(macOS)</code></li>
|
||
<li>导航至 <code>Languages & 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 # iOS(macOS 机器)
|
||
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;">依赖 & 代码生成</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 AAB(Google 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 SDK(NetworkSDK、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 + WebSocket,Flutter Plugin)
|
||
│ ├── build.yaml # @ApiRequest 代码生成器注册
|
||
│ └── lib/
|
||
│ ├── networks_sdk.dart # barrel file(统一导出)
|
||
│ └── src/
|
||
│ ├── annotations/
|
||
│ │ └── api_request.dart # @ApiRequest 注解定义
|
||
│ ├── generator/
|
||
│ │ ├── api_request_generator.dart # build_runner 代码生成器实现
|
||
│ │ └── builder.dart # SharedPartBuilder 入口
|
||
│ ├── data/
|
||
│ │ ├── datasources/
|
||
│ │ │ ├── http/
|
||
│ │ │ │ ├── api_client.dart # Dio REST 客户端
|
||
│ │ │ │ └── interceptor/
|
||
│ │ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
||
│ │ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
|
||
│ │ │ │ └── logging_interceptor.dart # 请求/响应日志
|
||
│ │ │ └── socket/
|
||
│ │ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream)
|
||
│ │ ├── dto/
|
||
│ │ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表
|
||
│ │ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||
│ │ └── repositories/
|
||
│ │ ├── networks_sdk_repository_impl.dart
|
||
│ │ └── networks_messaging_repository_impl.dart
|
||
│ ├── domain/
|
||
│ │ ├── entities/
|
||
│ │ │ ├── api_error.dart # @freezed HTTP 错误联合类型
|
||
│ │ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
||
│ │ │ ├── socket_connection_state.dart # 连接状态 enum
|
||
│ │ │ ├── http_method.dart # GET/POST/PUT/DELETE/PATCH
|
||
│ │ │ └── api_request_type.dart # request/login/upload
|
||
│ │ └── repositories/
|
||
│ │ ├── networks_sdk_repository.dart
|
||
│ │ └── networks_messaging_repository.dart
|
||
│ └── presentation/
|
||
│ ├── facade/
|
||
│ │ ├── networks_sdk_api.dart # HTTP 公开 API 接口
|
||
│ │ └── networks_messaging_api.dart # WebSocket 公开 API 接口
|
||
│ └── wiring/
|
||
│ ├── api_config.dart # HTTP 配置(baseURL/Token/回调)
|
||
│ ├── socket_config.dart # WebSocket 配置(心跳/重连策略)
|
||
│ ├── network_callbacks.dart # 回调类型定义
|
||
│ ├── networks_sdk_core.dart
|
||
│ ├── networks_sdk_api_impl.dart
|
||
│ ├── networks_messaging_api_impl.dart
|
||
│ └── networks_sdk_wiring.dart # 工厂:build() / buildMessagingApi()
|
||
│
|
||
├── cipher_guard_sdk/ # 端对端加密(Flutter Plugin)
|
||
│ └── lib/
|
||
│ ├── cipher_guard_sdk.dart # barrel file
|
||
│ └── src/
|
||
│ ├── data/
|
||
│ │ ├── constants/
|
||
│ │ │ └── method_channel_constants.dart
|
||
│ │ ├── datasources/
|
||
│ │ │ ├── encryption_flutter_service.dart # RSA/AES 纯 Dart 加解密(pointycastle + encrypt)
|
||
│ │ │ └── encryption_method_channel.dart # Native 密钥同步(iOS App Group)
|
||
│ │ ├── dto/
|
||
│ │ │ ├── chat_encryption_key_dto.dart
|
||
│ │ │ ├── encrypted_message_dto.dart
|
||
│ │ │ ├── method_channel_response.dart
|
||
│ │ │ └── rsa_key_pair_dto.dart
|
||
│ │ └── repositories/
|
||
│ │ └── encryption_repository_impl.dart
|
||
│ ├── domain/
|
||
│ │ ├── entities/
|
||
│ │ │ ├── chat_encryption_key.dart
|
||
│ │ │ ├── encrypted_message.dart
|
||
│ │ │ ├── rsa_key_pair.dart
|
||
│ │ │ └── session_key.dart
|
||
│ │ └── repositories/
|
||
│ │ └── encryption_repository.dart
|
||
│ └── presentation/
|
||
│ ├── facade/
|
||
│ │ └── cipher_guard_sdk_api.dart # 公开 API(调用方只依赖这里)
|
||
│ └── wiring/
|
||
│ ├── cipher_guard_sdk_api_impl.dart
|
||
│ ├── cipher_guard_sdk_core.dart
|
||
│ └── cipher_guard_sdk_wiring.dart
|
||
│
|
||
├── storage_sdk/ # 本地存储(Facade+Wiring,纯基础设施:连接管理 + 通用 CRUD)
|
||
│ └── lib/
|
||
│ ├── storage_sdk.dart # barrel:导出 StorageSdkApi
|
||
│ └── src/
|
||
│ ├── data/
|
||
│ │ ├── local/
|
||
│ │ │ └── datasources/
|
||
│ │ │ └── database_datasource.dart # 连接生命周期(openDatabase/closeDatabase)
|
||
│ │ └── repositories/
|
||
│ │ └── database_repository_impl.dart # 通用 CRUD 的 Drift 实现
|
||
│ ├── domain/
|
||
│ │ └── repositories/
|
||
│ │ └── database_repository.dart # 通用 CRUD 接口(泛型,与表无关)
|
||
│ └── presentation/
|
||
│ ├── facade/
|
||
│ │ └── storage_sdk_api.dart # 公开接口(生命周期 + 全量 CRUD)
|
||
│ └── wiring/
|
||
│ ├── storage_sdk_api_impl.dart # 委托给 Core
|
||
│ ├── storage_sdk_core.dart # 持有 DataSource + Repo
|
||
│ └── storage_sdk_wiring.dart # build(databaseFactory:) → StorageSdkApi
|
||
│
|
||
│ # ── 以下 SDK 均遵循相同的 Facade + Wiring 模式,结构参照 cipher_guard_sdk ──
|
||
│
|
||
├── notification_sdk/ # 推送通知(Flutter Plugin)
|
||
│ └── lib/notification_sdk.dart + src/{data,domain,presentation}/ (同上模式)
|
||
│
|
||
├── media_sdk/ # 媒体处理(Flutter Plugin)
|
||
│ └── lib/media_sdk.dart + src/{data,domain,presentation}/ (同上模式)
|
||
│
|
||
├── rtc_sdk/ # 实时音视频(Flutter Plugin)
|
||
│ └── lib/rtc_sdk.dart + src/{data,domain,presentation}/ (同上模式)
|
||
│
|
||
├── protocol_sdk/ # 消息协议(Flutter Plugin)
|
||
│ └── lib/protocol_sdk.dart + src/{data,domain,presentation}/ (同上模式)
|
||
│
|
||
├── l10n_sdk/ # 国际化(Flutter Plugin)
|
||
│ └── lib/l10n_sdk.dart + src/{data,domain,presentation}/ (同上模式)
|
||
</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<Message> messages;
|
||
final bool isLoading;
|
||
final String error;
|
||
|
||
const ChatState({
|
||
required this.messages,
|
||
required this.isLoading,
|
||
required this.error,
|
||
});
|
||
|
||
// 手写 copyWith - 每个字段都要写
|
||
ChatState copyWith({
|
||
List<Message>? messages,
|
||
bool? isLoading,
|
||
String? error,
|
||
}) {
|
||
return ChatState(
|
||
messages: messages ?? this.messages,
|
||
isLoading: isLoading ?? this.isLoading,
|
||
error: error ?? this.error,
|
||
);
|
||
}
|
||
|
||
// 手写 equality - 容易遗漏字段
|
||
@override
|
||
bool operator ==(Object other) {
|
||
if (identical(this, other)) return true;
|
||
return other is ChatState &&
|
||
listEquals(other.messages, messages) &&
|
||
other.isLoading == isLoading &&
|
||
other.error == error;
|
||
}
|
||
|
||
@override
|
||
int get hashCode => Object.hash(messages, isLoading, error);
|
||
}
|
||
|
||
// 手写 Provider - 样板代码多
|
||
final chatViewModelProvider = StateNotifierProvider.autoDispose<
|
||
ChatViewModel, ChatState
|
||
>((ref) {
|
||
final sendMessageUseCase = ref.watch(sendMessageUseCaseProvider);
|
||
final loadMessagesUseCase = ref.watch(loadMessagesUseCaseProvider);
|
||
return ChatViewModel(sendMessageUseCase, loadMessagesUseCase);
|
||
});
|
||
</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<Message> messages,
|
||
@Default(false) bool isLoading,
|
||
@Default('') String error,
|
||
}) = _ChatState;
|
||
}
|
||
|
||
// 使用 @riverpod - 自动生成 Provider
|
||
part 'chat_view_model.g.dart';
|
||
|
||
@riverpod
|
||
class ChatViewModel extends _$ChatViewModel {
|
||
@override
|
||
ChatState build() => const ChatState();
|
||
|
||
Future<void> sendMessage(String content) async {
|
||
state = state.copyWith(isLoading: true);
|
||
await ref.read(sendMessageUseCaseProvider)(content);
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
</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-状态管理">MVVM(Model-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(() => 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) => s.title));
|
||
|
||
// 2. Consumer:只重建 Consumer 包裹的部分
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
Text('静态内容,永远不重建'),
|
||
Consumer(
|
||
builder: (context, ref, child) {
|
||
final count = ref.watch(counterProvider);
|
||
return Text('动态内容: $count'); // 只有这里会重建
|
||
},
|
||
),
|
||
Text('静态内容,永远不重建'),
|
||
],
|
||
);
|
||
}
|
||
|
||
// 3. RepaintBoundary:隔离复杂动画,避免影响其他 Widget
|
||
RepaintBoundary(
|
||
child: ComplexAnimationWidget(), // 动画重绘不会影响外部
|
||
)
|
||
|
||
// 4. autoDispose:页面销毁时自动释放资源
|
||
final userProvider = StateNotifierProvider.autoDispose<UserViewModel, UserState>(
|
||
(ref) => UserViewModel(),
|
||
);
|
||
|
||
// 5. family:列表中每个 Item 有独立状态
|
||
final messageProvider = Provider.family<Message, int>((ref, messageId) {
|
||
return ref.watch(chatRepositoryProvider).getMessageById(messageId);
|
||
});
|
||
</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<ChatController>();
|
||
|
||
return Column(
|
||
children: [
|
||
// 问题1:UI 直接修改状态
|
||
ElevatedButton(
|
||
onPressed: () => controller.isLoading.value = true, // UI 直接改状态
|
||
),
|
||
|
||
// 问题2:不知道状态从哪里来,往哪里去
|
||
Obx(() => 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: () => 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<ChatController>(); // 如果忘记 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<BaseController>(ChatController());
|
||
final controller = Get.find<UserController>(); // 找到 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 = <Chat>[].obs;
|
||
final companyChatList = <Chat>[].obs;
|
||
final lockedChatList = <Chat>[].obs;
|
||
final RxBool isNavigating = false.obs;
|
||
final isInitializing = true.obs;
|
||
ValueNotifier<bool> isInitializingValue = ValueNotifier(true); // 混用!
|
||
final isShowSkeleton = false.obs;
|
||
ValueNotifier<double> offset = ValueNotifier(0); // 混用!
|
||
RxDouble offsetObx = 0.0.obs; // 重复状态!
|
||
RxDouble miniAppIconOpacityWhenShowingMiniApp = 1.0.obs;
|
||
RxDouble miniAppIconOpacityWhenShowingChatView = 0.01.obs;
|
||
RxDouble miniAppIconScale = 0.5.obs;
|
||
RxBool isShowingApplet = false.obs;
|
||
RxBool isMiniAppletDraggingIcon = false.obs;
|
||
RxBool isBeingHovered = false.obs;
|
||
RxBool isShowDeleteBar = true.obs;
|
||
// ... 还有 50+ 个响应式变量!
|
||
}</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(() => AnimatedPadding(
|
||
child: GetBuilder(
|
||
builder: (_) => Obx(
|
||
() => Stack(
|
||
children: [
|
||
Obx(
|
||
() => Container(
|
||
child: Obx(
|
||
() => Opacity(
|
||
opacity: controller.opacity.value,
|
||
child: Obx(
|
||
() => 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<void> loadChats() { ... }
|
||
Future<void> 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<ChatListController>();
|
||
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 + Obx(UUTalk 现状)</strong></p>
|
||
|
||
<p><strong>状态混乱:</strong></p>
|
||
<pre><code class="language-dart">class ChatListController extends GetxController {
|
||
final chatList = <Chat>[].obs;
|
||
final isLoading = false.obs;
|
||
ValueNotifier<double> offset = ValueNotifier(0); // 混用!
|
||
RxDouble offsetObx = 0.0.obs; // 重复!
|
||
RxBool isShowingApplet = false.obs;
|
||
RxBool isMiniAppletDraggingIcon = false.obs;
|
||
// ... 50+ 个零散变量
|
||
|
||
void loadChats() {
|
||
objectMgr.chatMgr.getChats(); // 全局依赖,不可测
|
||
}
|
||
}</code></pre>
|
||
|
||
<p><strong>UI 嵌套地狱:</strong></p>
|
||
<pre><code class="language-dart">Obx(() => GetBuilder(
|
||
builder: (_) => Obx(
|
||
() => Obx(
|
||
() => Obx(() => ...), // 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<Chat> chats,
|
||
@Default(false) bool isLoading,
|
||
}) = _ChatListState;
|
||
}
|
||
|
||
class ChatListViewModel extends StateNotifier<ChatListState> {
|
||
ChatListViewModel(this._chatRepository)
|
||
: super(const ChatListState());
|
||
|
||
final ChatRepository _chatRepository; // 依赖明确
|
||
|
||
Future<void> loadChats() async {
|
||
state = state.copyWith(isLoading: true);
|
||
final chats = await _chatRepository.getChats();
|
||
state = state.copyWith(chats: chats, isLoading: false);
|
||
}
|
||
}</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<T> {
|
||
/// API 路径
|
||
String get path;
|
||
|
||
/// HTTP 方法
|
||
HTTPMethod get method;
|
||
|
||
/// 序列化为 JSON(由 @JsonSerializable 自动生成)
|
||
Map<String, dynamic> toJson();
|
||
|
||
/// 自定义请求头
|
||
Map<String, String>? get customHeaders => null;
|
||
|
||
/// 请求类型(决定 header 处理方式)
|
||
APIRequestType get requestType => APIRequestType.request;
|
||
|
||
/// 解码响应(默认实现由扩展提供)
|
||
T? decodeResponse(Response response);
|
||
}
|
||
|
||
/// 默认实现 - parameters 自动调用 toJson()
|
||
extension APIRequestableDefaults<T> on APIRequestable<T> {
|
||
/// 请求参数(自动序列化,用户无需手动定义)
|
||
Map<String, dynamic>? get parameters {
|
||
// 对于 upload 类型,不序列化参数
|
||
if (requestType == APIRequestType.upload) {
|
||
return null;
|
||
}
|
||
// 自动调用 toJson() 序列化请求对象
|
||
return toJson();
|
||
}
|
||
}
|
||
</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<T?> executeRequest<T>(Ref ref, APIRequestable<T> request) async {
|
||
final dio = ref.read(apiClientProvider);
|
||
final config = ref.read(aPIConfigurationProvider);
|
||
|
||
// 1. 检查网络连接
|
||
if (!networkManager.isNetworkAvailable) {
|
||
throw const APIError.noNetworkConnection();
|
||
}
|
||
|
||
try {
|
||
// 2. 根据请求类型构建 header
|
||
final headers = configNotifier.defaultHeaders(
|
||
customHeaders: request.customHeaders,
|
||
includeToken: request.requestType != APIRequestType.login,
|
||
);
|
||
|
||
// 3. 执行请求
|
||
final response = await dio.request(
|
||
'${config.baseURL}${request.path}',
|
||
data: request.parameters,
|
||
options: Options(method: request.method.value, headers: headers),
|
||
);
|
||
|
||
// 4. 自动解码响应
|
||
return request.decodeResponse(response);
|
||
} on DioException catch (e) {
|
||
// 5. 统一错误处理
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
</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 = <Type, Function>{};
|
||
|
||
/// 注册响应类型 - 一次注册,全局可用
|
||
T Function(Map<String, dynamic>)? registerResponse<T>(
|
||
T Function(Map<String, dynamic>) fromJson,
|
||
) {
|
||
_fromJsonRegistry[T] = fromJson;
|
||
return fromJson;
|
||
}
|
||
|
||
/// 自动解码扩展 - 使用侧无需关心
|
||
extension APIRequestableExtension<T> on APIRequestable<T> {
|
||
T? decodeResponse(Response response) {
|
||
final data = response.data as Map<String, dynamic>;
|
||
|
||
// 从注册表查找 fromJson 函数
|
||
final fromJsonFunc = _fromJsonRegistry[T] as T Function(Map<String, dynamic>)?;
|
||
|
||
if (fromJsonFunc == null) {
|
||
throw StateError('fromJson not registered for type $T');
|
||
}
|
||
|
||
// 自动解码 APIResponseWrapper
|
||
final wrapper = APIResponseWrapper<T>.fromJson(
|
||
data,
|
||
(json) => fromJsonFunc(json as Map<String, dynamic>),
|
||
);
|
||
|
||
// 检查业务错误码
|
||
if (wrapper.code != 0) {
|
||
throw APIError.apiError(code: wrapper.code, message: wrapper.message);
|
||
}
|
||
|
||
return wrapper.data;
|
||
}
|
||
}
|
||
</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<String, dynamic> json) => _$LoginDataFromJson(json);
|
||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||
|
||
User toEntity() => User(id: userId, email: email); // DTO → Domain Entity
|
||
}
|
||
|
||
// ── Request ──
|
||
// @ApiRequest 自动生成 path / method / requestType / includeToken / fromJson 注册
|
||
|
||
@ApiRequest(
|
||
path: ApiPaths.authLogin, // 路径统一在 core/foundation/api_paths.dart 管理
|
||
method: HttpMethod.post,
|
||
responseType: LoginData,
|
||
requestType: ApiRequestType.login,
|
||
)
|
||
@JsonSerializable()
|
||
class LoginRequest extends ApiRequestable<LoginData> with _$LoginRequestApi {
|
||
final String email;
|
||
final String password;
|
||
|
||
LoginRequest({required this.email, required this.password});
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
|
||
}
|
||
</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<T></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<LoginData> 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<String, String>? 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<ApiRequest> {
|
||
@override
|
||
String generateForAnnotatedElement(
|
||
Element element,
|
||
ConstantReader annotation,
|
||
BuildStep buildStep,
|
||
) {
|
||
final className = element.name;
|
||
final path = annotation.read('path').stringValue;
|
||
final methodName = _readEnumName(annotation.read('method').objectValue, 'post');
|
||
final responseType = annotation.read('responseType').typeValue;
|
||
final responseTypeName = responseType.getDisplayString();
|
||
final requestTypeName = _readEnumName(annotation.read('requestType').objectValue, 'request');
|
||
|
||
// includeToken:默认 login → false,其余 → true
|
||
final includeTokenReader = annotation.peek('includeToken');
|
||
final includeToken = (includeTokenReader != null && !includeTokenReader.isNull)
|
||
? includeTokenReader.boolValue
|
||
: requestTypeName != 'login';
|
||
|
||
// 生成 mixin,使用侧只需 `with _$XxxApi`
|
||
return '''
|
||
/// Generated by @ApiRequest for [$className]
|
||
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
|
||
@override String get path => '$path';
|
||
@override HttpMethod get method => HttpMethod.$methodName;
|
||
@override ApiRequestType get requestType => ApiRequestType.$requestTypeName;
|
||
@override bool get includeToken => $includeToken;
|
||
@override
|
||
Map<String, dynamic>? get parameters {
|
||
registerResponse<$responseTypeName>($responseTypeName.fromJson);
|
||
return super.parameters;
|
||
}
|
||
}
|
||
''';
|
||
}
|
||
}
|
||
</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<T> 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<String, dynamic> json) =>
|
||
_$SendMessageDataFromJson(json);
|
||
}
|
||
|
||
// ── Request ──
|
||
@ApiRequest(path: ApiPaths.chatSendMessage, responseType: SendMessageData)
|
||
@JsonSerializable()
|
||
class SendMessageRequest extends ApiRequestable<SendMessageData>
|
||
with _$SendMessageRequestApi {
|
||
@JsonKey(name: 'chat_id')
|
||
final String chatId;
|
||
final String content;
|
||
|
||
SendMessageRequest({required this.chatId, required this.content});
|
||
@override
|
||
Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
|
||
}
|
||
</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<String, dynamic> json) =>
|
||
_$ProfileDataFromJson(json);
|
||
|
||
User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
|
||
}
|
||
|
||
@ApiRequest(path: ApiPaths.userProfile, method: HttpMethod.get, responseType: ProfileData)
|
||
@JsonSerializable()
|
||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||
with _$GetProfileRequestApi {
|
||
GetProfileRequest(); // 无参数 — GET /user/profile 靠 token 获取当前用户
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
||
}
|
||
</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<String, dynamic> json) =>
|
||
_$UploadResultFromJson(json);
|
||
}
|
||
|
||
@ApiRequest(
|
||
path: ApiPaths.uploadFile,
|
||
method: HttpMethod.post,
|
||
responseType: UploadResult,
|
||
requestType: ApiRequestType.upload,
|
||
)
|
||
class UploadFileRequest extends ApiRequestable<UploadResult>
|
||
with _$UploadFileRequestApi {
|
||
final String filePath;
|
||
final String? fileName;
|
||
|
||
UploadFileRequest({required this.filePath, this.fileName});
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => {}; // upload 不走 toJson
|
||
|
||
@override
|
||
Object? get uploadData => FormData.fromMap({
|
||
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
|
||
});
|
||
}
|
||
</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<T></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<ApiConfig>((ref) {
|
||
return ApiConfig(
|
||
baseURL: AppConfig.apiBaseUrl,
|
||
platformHeaders: {'Platform': 'Android', 'client-version': '1.0.0'},
|
||
tokenExpiredCodes: {30002, 30003, 30124},
|
||
forceLogoutCodes: {30125},
|
||
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
|
||
onTokenRefresh: () async { /* 刷新 token */ return null; },
|
||
onLog: (message, {tag}) { print('[$tag] $message'); },
|
||
);
|
||
});
|
||
|
||
/// 2. API 客户端(内部自动挂载 Auth / Retry / Logging 拦截器)
|
||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||
return ApiClient(config: ref.read(apiConfigProvider));
|
||
});
|
||
|
||
// ── features/auth/di/auth_providers.dart ── (Auth 模块完整 DI 链路)
|
||
|
||
/// 3. Repository(注入 domain 接口类型,ViewModel 不感知具体实现)
|
||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||
final apiConfig = ref.read(apiConfigProvider);
|
||
return AuthRepositoryImpl(
|
||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
||
onTokenUpdate: (token) {
|
||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||
},
|
||
);
|
||
});
|
||
|
||
/// 4. UseCase(按需)
|
||
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
||
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
||
});
|
||
</code></pre>
|
||
|
||
<p><strong>ViewModel 中使用</strong>:</p>
|
||
|
||
<pre><code class="language-dart">@riverpod
|
||
class LoginViewModel extends _$LoginViewModel {
|
||
@override
|
||
LoginState build() => const LoginState();
|
||
|
||
Future<void> login(String email, String password) async {
|
||
state = state.copyWith(isLoading: true);
|
||
|
||
try {
|
||
// UseCase 封装格式校验 + 业务编排,ViewModel 只需一行
|
||
final user = await ref.read(loginUseCaseProvider).execute(
|
||
email: email, password: password);
|
||
|
||
state = state.copyWith(user: user, isLoading: false);
|
||
} on FormatException catch (e) {
|
||
// 格式校验失败(UseCase 层抛出)
|
||
state = state.copyWith(error: e.message, isLoading: false);
|
||
} on ApiError catch (e) {
|
||
// 统一错误处理 - Freezed union type
|
||
state = state.copyWith(error: e.displayMessage, isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
</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) ← 反序列化
|
||
← LoginData(DTO)
|
||
→ 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 # routerProvider:StatefulShellRoute + 全局 redirect
|
||
│ │ ├── app_route_name.dart # AppRouteName 枚举,路径常量 + fromPath()
|
||
│ │ └── guards/
|
||
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
|
||
│ │
|
||
│ └── di/ # 全局 DI — 手动装配的 Provider
|
||
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + ApiClient + SocketConfig + SocketClient + SocketManager
|
||
│ └── app_providers.dart # 全局共享状态(themeModeProvider + AuthNotifier)
|
||
│
|
||
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
|
||
│ │
|
||
│ ├── app_tab/ # Tab 容器(底部导航栏)
|
||
│ │ └── view/
|
||
│ │ └── app_tab.dart # StatefulShellRoute 子壳 + 底部导航逻辑
|
||
│ │
|
||
│ ├── login/ # 登录 ── 已实现
|
||
│ │ ├── di/
|
||
│ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider
|
||
│ │ ├── presentation/
|
||
│ │ │ ├── login_view_model.dart # @riverpod ViewModel(生成 login_view_model.g.dart)
|
||
│ │ │ └── login_state.dart # @freezed State(生成 login_state.freezed.dart)
|
||
│ │ ├── usecases/
|
||
│ │ │ └── login_usecase.dart # 格式校验 → Repository → User Entity
|
||
│ │ └── view/
|
||
│ │ └── login_page.dart # 登录页
|
||
│ │
|
||
│ ├── chat/ # 聊天 ── 开发中
|
||
│ │ ├── presentation/
|
||
│ │ │ └── chat_view_model.dart # @riverpod ViewModel
|
||
│ │ └── view/
|
||
│ │ ├── chat_page.dart # 会话列表页(Tab 1)
|
||
│ │ └── chat_detail_page.dart # 聊天详情页
|
||
│ │
|
||
│ ├── contact/ # 通讯录 ── 骨架
|
||
│ │ └── view/
|
||
│ │ └── contact_page.dart # 通讯录页(Tab 2)
|
||
│ │
|
||
│ └── settings/ # 设置 ── 已实现(主题切换)
|
||
│ ├── di/
|
||
│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入)
|
||
│ ├── presentation/
|
||
│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart)
|
||
│ ├── usecases/
|
||
│ │ └── set_theme_usecase.dart # 主题切换用例
|
||
│ └── view/
|
||
│ ├── settings_page.dart # 设置主页(Tab 3)
|
||
│ ├── theme_view.dart # 主题选择页
|
||
│ └── widgets/
|
||
│ ├── settings_section_header.dart
|
||
│ └── theme_option_tile.dart
|
||
│
|
||
├── domain/ # Domain 层(纯 Dart,零 Flutter / 零网络依赖)
|
||
│ ├── entities/
|
||
│ │ └── user.dart # 用户实体
|
||
│ │ # message / conversation / contact 待开发
|
||
│ └── repositories/
|
||
│ └── auth_repository.dart # abstract interface
|
||
│ # message / chat / contact_repository 待开发
|
||
│
|
||
├── data/ # Data 层(implements domain 接口)
|
||
│ ├── repositories/
|
||
│ │ └── auth_repository_impl.dart # 认证仓库
|
||
│ │ # message / chat / contact 待开发
|
||
│ ├── local/
|
||
│ │ └── drift/ # Drift 本地数据库
|
||
│ │ ├── app_database.dart # @DriftDatabase 定义 + onUpgrade 自动补列
|
||
│ │ # database_connection.dart 已迁移至 storage_sdk(数据库生命周期统一在 SDK 层管理)
|
||
│ │ ├── mapper/
|
||
│ │ │ └── drift_path_mapper.dart # Drift 路径映射工具
|
||
│ │ └── tables/
|
||
│ │ └── users.dart # Users 表定义
|
||
│ ├── remote/ # Request 文件(一个端点一个文件)
|
||
│ │ ├── login_request.dart # 登录
|
||
│ │ ├── logout_request.dart # 登出
|
||
│ │ ├── get_profile_request.dart # 获取用户信息
|
||
│ │ └── upload_file_request.dart # 文件上传
|
||
│ │ # send_message / 其他业务端点 待开发
|
||
│ └── models/ # 持久化 DTO(@JsonSerializable)
|
||
│ └── user_dto.dart # 用户持久化 DTO
|
||
│ # message / conversation / contact_dto 待开发
|
||
│
|
||
└── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
|
||
├── foundation/ # 基础配置(各为单独文件,非子目录)
|
||
│ ├── api_paths.dart # API 路径常量(ApiPaths.authLogin 等)
|
||
│ ├── config.dart # 运行时配置(AppConfig,通过 --dart-define-from-file 注入)
|
||
│ ├── constants.dart # 全局常量(AppConstants:重试次数 / 退避延迟 / 超时等)
|
||
│ ├── errors.dart # 统一异常体系
|
||
│ ├── extensions.dart # Dart 扩展方法(String / DateTime / List 等)
|
||
│ ├── logger.dart # 日志门面(分级日志,生产环境自动关闭 debug)
|
||
│ ├── types.dart # 通用类型(Result<T> / typedefs / Unit)
|
||
│ └── utils.dart # 工具函数(纯函数,无副作用)
|
||
│
|
||
├── services/ # 跨模块服务(有状态,作为独立 Provider)
|
||
│ ├── app_initializer.dart # 启动初始化编排(按序初始化各依赖)
|
||
│ ├── network_backoff_debouncer.dart # 网络恢复退避防抖(4s→8s→...→60s,2min 重置)
|
||
│ ├── network_monitor.dart # 网络状态监听(connectivity_plus)
|
||
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
|
||
│
|
||
└── ui/ # Core UI(设计系统 + 可复用组件)
|
||
├── base/ # 设计 Token
|
||
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
|
||
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
|
||
│ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors)
|
||
│ └── font.dart # 字体(TextStyle 定义 + textTheme(brightness))
|
||
├── components/ # 原子组件
|
||
│ └── app_button.dart # 按钮
|
||
│ # app_text_field / app_avatar / app_badge 等 待开发
|
||
└── composites/ # 组合组件(目录预留,待开发)
|
||
# app_dialog / app_toast / app_empty_state 等
|
||
</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<State><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 Models(MVVM 的 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>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + 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<List<Message>> getMessages(String chatId);
|
||
Future<void> sendMessage(Message message);
|
||
}
|
||
|
||
// Data 层实现接口
|
||
class ChatRepositoryImpl implements ChatRepository {
|
||
final ApiClient _client;
|
||
final MessageLocalDataSource _localDataSource;
|
||
|
||
@override
|
||
Future<List<Message>> getMessages(String chatId) async {
|
||
// 实现数据获取逻辑
|
||
}
|
||
}
|
||
|
||
// Presentation 层使用接口
|
||
class ChatViewModel {
|
||
final ChatRepository _repository; // 依赖接口,不依赖实现
|
||
|
||
Future<void> loadMessages() async {
|
||
final messages = await _repository.getMessages(chatId);
|
||
// ...
|
||
}
|
||
}
|
||
</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<String>? onChanged;
|
||
|
||
const SearchBar({
|
||
required this.hint,
|
||
this.onChanged,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: EdgeInsets.all(AppTokens.spacing12),
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.search, color: AppColors.gray500),
|
||
SizedBox(width: AppTokens.spacing8),
|
||
Expanded(
|
||
child: AppTextField(
|
||
hint: hint,
|
||
controller: TextEditingController(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
</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<Widget>? actions;
|
||
|
||
const PlatformAppBar({
|
||
required this.title,
|
||
this.actions,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// iOS 使用 CupertinoNavigationBar
|
||
if (PlatformAdapter.isIOS) {
|
||
return CupertinoNavigationBar(
|
||
middle: Text(title),
|
||
trailing: actions != null ? Row(children: actions!) : null,
|
||
);
|
||
}
|
||
// Android 使用 Material AppBar
|
||
else {
|
||
return AppBar(
|
||
title: Text(title),
|
||
actions: actions,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Size get preferredSize => Size.fromHeight(56);
|
||
}
|
||
|
||
/// 平台自适应对话框
|
||
class PlatformDialog {
|
||
static Future<bool?> showConfirm(
|
||
BuildContext context, {
|
||
required String title,
|
||
required String content,
|
||
}) {
|
||
// iOS 使用 CupertinoAlertDialog
|
||
if (PlatformAdapter.isIOS) {
|
||
return showCupertinoDialog<bool>(
|
||
context: context,
|
||
builder: (context) => CupertinoAlertDialog(
|
||
title: Text(title),
|
||
content: Text(content),
|
||
actions: [
|
||
CupertinoDialogAction(
|
||
child: Text('取消'),
|
||
onPressed: () => Navigator.pop(context, false),
|
||
),
|
||
CupertinoDialogAction(
|
||
child: Text('确定'),
|
||
isDestructiveAction: true,
|
||
onPressed: () => Navigator.pop(context, true),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
// Android 使用 Material AlertDialog
|
||
else {
|
||
return showDialog<bool>(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: Text(title),
|
||
content: Text(content),
|
||
actions: [
|
||
TextButton(
|
||
child: Text('取消'),
|
||
onPressed: () => Navigator.pop(context, false),
|
||
),
|
||
TextButton(
|
||
child: Text('确定'),
|
||
onPressed: () => Navigator.pop(context, true),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
</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<ContextMenuItem> menuItems;
|
||
|
||
const PlatformContextMenu({
|
||
required this.child,
|
||
required this.menuItems,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 桌面端支持右键菜单
|
||
if (PlatformAdapter.isDesktop) {
|
||
return GestureDetector(
|
||
onSecondaryTapDown: (details) {
|
||
_showContextMenu(context, details.globalPosition);
|
||
},
|
||
child: child,
|
||
);
|
||
}
|
||
|
||
// 移动端使用长按显示菜单
|
||
return GestureDetector(
|
||
onLongPress: () {
|
||
_showMobileMenu(context);
|
||
},
|
||
child: child,
|
||
);
|
||
}
|
||
|
||
void _showContextMenu(BuildContext context, Offset position) {
|
||
// 显示桌面端右键菜单
|
||
}
|
||
|
||
void _showMobileMenu(BuildContext context) {
|
||
// 显示移动端底部菜单
|
||
}
|
||
}
|
||
</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 用 CupertinoNavigationBar,Android 用 AppBar</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>对话框</strong></td>
|
||
<td>PlatformDialog</td>
|
||
<td>iOS 用 CupertinoAlertDialog,Android 用 AlertDialog</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>按钮</strong></td>
|
||
<td>PlatformButton</td>
|
||
<td>iOS 用 CupertinoButton,Android 用 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: (_) => 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 时覆盖整个 Shell(TabBar 消失)
|
||
final _rootKey = GlobalKey<NavigatorState>();
|
||
|
||
final routerProvider = Provider<GoRouter>((ref) {
|
||
final authNotifier = ref.read(authNotifierProvider);
|
||
|
||
return GoRouter(
|
||
// Root Navigator 的 Key,供全屏路由声明 parentNavigatorKey 使用
|
||
navigatorKey: _rootKey,
|
||
|
||
// 冷启动默认落地页;authGuard 会在进入前检查登录状态并按需重定向
|
||
initialLocation: AppRouteName.chat.path,
|
||
|
||
// 在控制台打印每次路由变化,方便开发期间调试;上线前设为 false
|
||
debugLogDiagnostics: true,
|
||
|
||
// 监听 authNotifier:登录 / 退出时自动触发 redirect 重新执行
|
||
refreshListenable: authNotifier,
|
||
|
||
redirect: (context, state) => authGuard(authNotifier, state),
|
||
|
||
routes: [
|
||
// ── Shell 内:底部导航栏始终可见 ─────────────────────────────────
|
||
StatefulShellRoute.indexedStack(
|
||
builder: (context, state, navigationShell) {
|
||
return AppTab(navigationShell: navigationShell);
|
||
},
|
||
branches: [
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(path: AppRouteName.chat.path, builder: (_, __) => const ChatPage()),
|
||
]),
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(path: AppRouteName.contact.path, builder: (_, __) => const ContactPage()),
|
||
]),
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(path: AppRouteName.settings.path, builder: (_, __) => const SettingsPage()),
|
||
]),
|
||
],
|
||
),
|
||
|
||
// ── Shell 外:全屏页面,无底部导航栏 ─────────────────────────────
|
||
// parentNavigatorKey: _rootKey 确保路由覆盖 Shell,TabBar 消失
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.chatDetail.path,
|
||
builder: (context, state) {
|
||
final extra = state.extra as ({String conversationId, String title});
|
||
return ChatDetailPage(conversationId: extra.conversationId, title: extra.title);
|
||
},
|
||
),
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.chatDetailById.path,
|
||
builder: (context, state) {
|
||
final id = state.pathParameters['id']!;
|
||
return ChatDetailPage(conversationId: id, title: '路径参数详情');
|
||
},
|
||
),
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.settingsTheme.path,
|
||
builder: (_, __) => const ThemeView(),
|
||
),
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.login.path,
|
||
builder: (_, __) => const LoginPage(),
|
||
),
|
||
],
|
||
);
|
||
});
|
||
</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>StatefulShellRoute(go_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>StatefulShellRoute(Shell 内,TabBar 可见)
|
||
branches[0] → /chat ← 聊天 Tab(独立栈)
|
||
branches[1] → /contact ← 联系人 Tab(独立栈)
|
||
branches[2] → /settings ← 设置 Tab(独立栈)
|
||
|
||
Root Navigator(Shell 外,TabBar 隐藏,parentNavigatorKey: _rootKey)
|
||
/chat/detail ← extra 传参详情页
|
||
/chat/:id ← 路径参数详情页
|
||
/settings/theme ← 主题设置页
|
||
/login ← 登录页
|
||
</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>切换 Tab(TabBar 可见)</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'));
|
||
|
||
// 切换 Tab(TabBar 可见,不可返回)
|
||
context.go(AppRouteName.contact.path);
|
||
|
||
// 返回
|
||
context.pop();
|
||
|
||
// 返回并传值
|
||
final result = await context.push<String>(AppRouteName.settingsTheme.path);
|
||
|
||
// 弹窗(Flutter 原生)
|
||
showDialog(context: context, builder: (_) => const AlertDialog(...));
|
||
</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 覆盖整个 Shell,TabBar 隐藏
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.chatDetail.path, // '/chat/detail'
|
||
builder: (context, state) {
|
||
// 路由层唯一做类型转换的地方,目标页面只知道构造参数
|
||
final extra = state.extra as ({String conversationId, String title});
|
||
return ChatDetailPage(
|
||
conversationId: extra.conversationId,
|
||
title: extra.title,
|
||
);
|
||
},
|
||
),
|
||
</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-如何切换">AppTab:Tab 如何切换</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) => 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) => 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.dart(Shell 外,全屏)
|
||
GoRoute(
|
||
parentNavigatorKey: _rootKey,
|
||
path: AppRouteName.profile.path,
|
||
builder: (_, __) => const ProfilePage(),
|
||
),
|
||
</code></pre>
|
||
|
||
<p>Tab 内子页面(保留 TabBar):加到对应 <code>StatefulShellBranch</code> 的 <code>routes</code> 里,<strong>不加</strong> <code>parentNavigatorKey</code>,路由放到 Branch Navigator,TabBar 保持可见。</p>
|
||
<pre><code class="language-dart">// app_router.dart(Shell 内,TabBar 可见)
|
||
StatefulShellBranch(
|
||
routes: [
|
||
GoRoute(
|
||
path: AppRouteName.settings.path,
|
||
builder: (_, __) => const SettingsPage(),
|
||
routes: [
|
||
// 此处不加 parentNavigatorKey,路由在 Branch Navigator 内
|
||
GoRoute(path: AppRouteName.profile.segment, builder: (_, __) => const ProfilePage()),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
</code></pre>
|
||
|
||
<p><strong>Step 4:在需要的地方跳转</strong></p>
|
||
<pre><code class="language-dart">onTap: () => context.push(AppRouteName.profile.path),
|
||
</code></pre>
|
||
|
||
<h3 id="路由守卫接入正式-token">接入正式 token(storage_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<void> initialize() async {
|
||
final token = await secureStorage.read('token');
|
||
_isLoggedIn = token != null && token.isNotEmpty;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> login(String token) async {
|
||
await secureStorage.write('token', token);
|
||
_isLoggedIn = true;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> logout() async {
|
||
await secureStorage.delete('token');
|
||
_isLoggedIn = false;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
</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<Message> messages,
|
||
@Default(false) bool isLoading,
|
||
@Default('') String error,
|
||
Message? selectedMessage,
|
||
}) = _ChatState;
|
||
}
|
||
|
||
// 2. 定义 ViewModel — 直接调用 Repository
|
||
class ChatViewModel extends StateNotifier<ChatState> {
|
||
ChatViewModel(this._chatRepository) : super(const ChatState());
|
||
|
||
final ChatRepository _chatRepository;
|
||
|
||
// 发送消息
|
||
Future<void> sendMessage(String content) async {
|
||
state = state.copyWith(isLoading: true, error: '');
|
||
|
||
try {
|
||
await _chatRepository.sendMessage(content);
|
||
await loadMessages(); // 重新加载消息列表
|
||
} catch (e) {
|
||
state = state.copyWith(error: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
|
||
// 加载消息
|
||
Future<void> loadMessages() async {
|
||
state = state.copyWith(isLoading: true);
|
||
|
||
try {
|
||
final messages = await _chatRepository.getMessages();
|
||
state = state.copyWith(messages: messages);
|
||
} catch (e) {
|
||
state = state.copyWith(error: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
|
||
// 选择消息
|
||
void selectMessage(Message message) {
|
||
state = state.copyWith(selectedMessage: message);
|
||
}
|
||
}
|
||
|
||
// 3. 定义 Provider
|
||
final chatViewModelProvider =
|
||
StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
|
||
return ChatViewModel(ref.watch(chatRepositoryProvider));
|
||
});
|
||
</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<Message> messages,
|
||
@Default(false) bool isLoading,
|
||
@Default('') String error,
|
||
}) = _ChatState;
|
||
}
|
||
|
||
@riverpod
|
||
class ChatViewModel extends _$ChatViewModel {
|
||
@override
|
||
ChatState build() => const ChatState();
|
||
|
||
// ViewModel 直接调用 Repository
|
||
Future<void> sendMessage(String content) async {
|
||
state = state.copyWith(isLoading: true);
|
||
try {
|
||
await ref.read(chatRepositoryProvider).sendMessage(content);
|
||
await loadMessages();
|
||
} catch (e) {
|
||
state = state.copyWith(error: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
|
||
Future<void> loadMessages() async {
|
||
state = state.copyWith(isLoading: true);
|
||
try {
|
||
final messages = await ref.read(chatRepositoryProvider).getMessages();
|
||
state = state.copyWith(messages: messages);
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
</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) => s.error), (prev, next) {
|
||
if (next.isNotEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text(next)),
|
||
);
|
||
}
|
||
});
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('聊天')),
|
||
body: Column(
|
||
children: [
|
||
// 消息列表
|
||
Expanded(
|
||
child: state.isLoading
|
||
? const Center(child: CircularProgressIndicator())
|
||
: ListView.builder(
|
||
itemCount: state.messages.length,
|
||
itemBuilder: (context, index) {
|
||
final message = state.messages[index];
|
||
return MessageBubble(message: message);
|
||
},
|
||
),
|
||
),
|
||
|
||
// 输入框
|
||
ChatInputArea(
|
||
onSend: (content) => viewModel.sendMessage(content),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
</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 专属 Domain(features/*/domain/)</h4>
|
||
|
||
<ul>
|
||
<li><strong>Use Cases</strong>:封装该 Feature 的业务逻辑</li>
|
||
<li><strong>Entities</strong>:该 Feature 特有的Domain 实体</li>
|
||
</ul>
|
||
|
||
<h4>全局共享 Domain(domain/)</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 DataSource(data/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<List<MessageDTO>> 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<SendMessageData?> sendMessage({
|
||
required String chatId,
|
||
required String content,
|
||
}) {
|
||
return _client.executeRequest(
|
||
SendMessageRequest(chatId: chatId, content: content),
|
||
);
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<h4>Cache Manager(data/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 Models(data/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<String, dynamic> 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>Constants(core/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>Config(core/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>Errors(core/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>Logger(core/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>Types(core/foundation/types/)</h4>
|
||
|
||
<ul>
|
||
<li><code>result.dart</code>:Result<T> 类型(Success / Failure)</li>
|
||
<li><code>either.dart</code>:Either<L, R> 类型</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 SDK(packages/networks_sdk/)</h4>
|
||
|
||
<p>HTTP + WebSocket 客户端 SDK(Flutter 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<T> 唯一入口)
|
||
│ │ │ └── interceptor/
|
||
│ │ │ ├── auth_interceptor.dart # Token + 默认 header 注入
|
||
│ │ │ ├── retry_interceptor.dart # Token 刷新 + 瞬态错误重试
|
||
│ │ │ └── logging_interceptor.dart # 请求/响应日志
|
||
│ │ └── socket/
|
||
│ │ └── socket_client.dart # WebSocket 长连接(心跳/重连/Stream 输出)
|
||
│ ├── dto/
|
||
│ │ ├── api_requestable.dart # 请求基类 + fromJson 注册表 + 解码扩展
|
||
│ │ └── api_response_wrapper.dart # { code, message/msg, data } 信封解析
|
||
│ └── repositories/
|
||
│ ├── networks_sdk_repository_impl.dart
|
||
│ └── networks_messaging_repository_impl.dart
|
||
├── domain/
|
||
│ ├── entities/
|
||
│ │ ├── api_error.dart # @freezed HTTP 错误联合类型
|
||
│ │ ├── socket_error.dart # @freezed WebSocket 错误联合类型
|
||
│ │ ├── socket_connection_state.dart # 连接状态 enum
|
||
│ │ ├── http_method.dart # GET / POST / PUT / DELETE / PATCH
|
||
│ │ └── api_request_type.dart # request / login / upload
|
||
│ └── repositories/
|
||
│ ├── networks_sdk_repository.dart
|
||
│ └── networks_messaging_repository.dart
|
||
└── presentation/
|
||
├── facade/
|
||
│ ├── networks_sdk_api.dart # HTTP 公开 API 接口
|
||
│ └── networks_messaging_api.dart # WebSocket 公开 API 接口
|
||
└── wiring/
|
||
├── api_config.dart # HTTP 配置(baseURL / token / 回调)
|
||
├── socket_config.dart # WebSocket 配置(心跳 / 重连策略)
|
||
├── network_callbacks.dart # 回调类型定义(OnTokenRefresh 等)
|
||
├── networks_sdk_core.dart
|
||
├── networks_sdk_api_impl.dart
|
||
├── networks_messaging_api_impl.dart
|
||
└── networks_sdk_wiring.dart # 工厂:build() / buildMessagingApi()
|
||
</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_id,Dart 字段名是 userId
|
||
final String userId;
|
||
final String email;
|
||
final String? nickname; // 可选字段用 String?
|
||
final String? avatar;
|
||
|
||
const LoginData({ // 构造函数,参数和字段一一对应
|
||
required this.token,
|
||
required this.userId,
|
||
required this.email,
|
||
this.nickname,
|
||
this.avatar,
|
||
});
|
||
|
||
// ↓ 这两行是固定写法,照抄就行,把类名替换掉
|
||
factory LoginData.fromJson(Map<String, dynamic> json) =>
|
||
_$LoginDataFromJson(json); // ← 短暂报红,watch 模式下保存后几秒自动消失
|
||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||
|
||
/// DTO → Domain Entity(把网络层数据转为业务层数据)
|
||
User toEntity() {
|
||
return User(
|
||
id: userId,
|
||
email: email,
|
||
nickname: nickname,
|
||
avatar: avatar,
|
||
);
|
||
}
|
||
}
|
||
</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<LoginData> // ← 固定写法:extends ApiRequestable<响应类型>
|
||
with _$LoginRequestApi { // ← 固定写法:with _$类名Api(短暂报红,保存后自动消失)
|
||
final String email; // 请求参数:要发给服务端的字段
|
||
final String password;
|
||
|
||
LoginRequest({required this.email, required this.password});
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this); // ← 固定写法,短暂报红,保存后自动消失
|
||
}
|
||
</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<User> login({
|
||
required String email,
|
||
required String password,
|
||
}) async {
|
||
// 1. 直接调 ApiClient,构造请求 → 发 HTTP → 自动解码 → 返回 DTO
|
||
final LoginData? loginData = await _client.executeRequest(
|
||
LoginRequest(email: email, password: password),
|
||
);
|
||
|
||
if (loginData == null) {
|
||
throw Exception('Login failed: empty response');
|
||
}
|
||
|
||
// 2. 回调写入 Token(内存 + 持久化由 Provider 层组合)
|
||
_onTokenUpdate(loginData.token);
|
||
|
||
// 3. DTO → Domain Entity,返回给上层
|
||
return loginData.toEntity();
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<!-- ────────── 第 3 步 ────────── -->
|
||
|
||
<h6>第 3 步:注册 Provider + 编写 ViewModel</h6>
|
||
|
||
<p><strong>3.1 注册 Provider(DI 装配)</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<AuthRepository>((ref) {
|
||
final apiConfig = ref.read(apiConfigProvider);
|
||
return AuthRepositoryImpl(
|
||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
||
onTokenUpdate: (token) {
|
||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||
},
|
||
);
|
||
});
|
||
|
||
// UseCase(按需 — 登录有多步编排,所以需要 UseCase)
|
||
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
||
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
||
});
|
||
</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<MessageRepository>((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() => const LoginState();
|
||
|
||
Future<void> login(String email, String password) async {
|
||
state = state.copyWith(isLoading: true);
|
||
|
||
try {
|
||
// 直接调 Repository
|
||
final user = await ref.read(authRepositoryProvider).login(
|
||
email: email,
|
||
password: password,
|
||
);
|
||
|
||
state = state.copyWith(user: user, isLoading: false);
|
||
} on ApiError catch (e) {
|
||
state = state.copyWith(error: e.displayMessage, isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<p><strong>进阶写法:有 UseCase 时(如登录需格式校验 + 多步编排)</strong></p>
|
||
|
||
<pre><code class="language-dart">@riverpod
|
||
class LoginViewModel extends _$LoginViewModel {
|
||
@override
|
||
LoginState build() => const LoginState();
|
||
|
||
Future<void> login(String email, String password) async {
|
||
state = state.copyWith(isLoading: true);
|
||
|
||
try {
|
||
// 通过 UseCase 调用(格式校验 → 登录 → 返回 User)
|
||
final user = await ref.read(loginUseCaseProvider).execute(
|
||
email: email,
|
||
password: password,
|
||
);
|
||
|
||
state = state.copyWith(user: user, isLoading: false);
|
||
} on FormatException catch (e) {
|
||
// 格式校验失败(UseCase 层抛出)
|
||
state = state.copyWith(error: e.message, isLoading: false);
|
||
} on ApiError catch (e) {
|
||
// 网络错误统一处理
|
||
state = state.copyWith(error: e.displayMessage, isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
</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 // 自动解码
|
||
← LoginData(DTO)
|
||
→ onTokenUpdate(token) // 回调:内存写入 + 持久化
|
||
← loginData.toEntity() → User(Domain 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<String, dynamic> json) =>
|
||
_$SendMessageDataFromJson(json);
|
||
}
|
||
|
||
// ── Request ──
|
||
|
||
@ApiRequest(
|
||
path: ApiPaths.chatSendMessage, // 路径常量,定义在 api_paths.dart
|
||
method: HttpMethod.post, // HTTP 方法
|
||
responseType: SendMessageData, // 响应类型
|
||
// requestType 不写,默认 ApiRequestType.request(会携带 Token)
|
||
)
|
||
@JsonSerializable()
|
||
class SendMessageRequest extends ApiRequestable<SendMessageData>
|
||
with _$SendMessageRequestApi {
|
||
@JsonKey(name: 'chat_id') // JSON 字段名和 Dart 字段名不一样时用 @JsonKey
|
||
final String chatId;
|
||
final String content;
|
||
|
||
SendMessageRequest({required this.chatId, required this.content});
|
||
@override
|
||
Map<String, dynamic> toJson() => _$SendMessageRequestToJson(this);
|
||
}
|
||
</code></pre>
|
||
|
||
<p>保存 → 自动生成 → 然后在 Repository 中直接调 ApiClient 就完了:</p>
|
||
|
||
<pre><code class="language-dart">// 在 MessageRepositoryImpl 中添加
|
||
Future<SendMessageData?> 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<String, dynamic> json) =>
|
||
_$ProfileDataFromJson(json);
|
||
|
||
User toEntity() => User(id: userId, email: email, nickname: nickname, avatar: avatar);
|
||
}
|
||
|
||
@ApiRequest(
|
||
path: ApiPaths.userProfile,
|
||
method: HttpMethod.get, // ← GET 请求,toJson() 结果作为 query string
|
||
responseType: ProfileData,
|
||
)
|
||
@JsonSerializable()
|
||
class GetProfileRequest extends ApiRequestable<ProfileData>
|
||
with _$GetProfileRequestApi {
|
||
GetProfileRequest(); // 无参数 — token 标识当前用户,无需显式传 user_id
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => _$GetProfileRequestToJson(this);
|
||
}
|
||
</code></pre>
|
||
|
||
<!-- ────────── 无响应数据示例 ────────── -->
|
||
|
||
<h6>无响应数据的接口(POST /auth/logout)</h6>
|
||
|
||
<p>有些接口不返回 data 字段,只有 <code>{"code": 0, "message": "ok"}</code>。这种情况用 <code>ApiRequestable<void></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<void> {
|
||
@override
|
||
String get path => ApiPaths.authLogout;
|
||
@override
|
||
HttpMethod get method => HttpMethod.post;
|
||
@override
|
||
Map<String, dynamic> toJson() => {}; // 无请求体
|
||
}
|
||
</code></pre>
|
||
|
||
<p>Repository 调用时直接 <code>await</code>:</p>
|
||
|
||
<pre><code class="language-dart">Future<void> 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>返回 null(SDK 自动跳过)</td></tr>
|
||
<tr><td>响应解码</td><td>标准 <code>{ code, msg, data }</code></td><td>可能需要 override <code>decodeResponse</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p><strong>模式 A:FormData 上传到自有后端</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<UploadResult>
|
||
with _$UploadFileRequestApi {
|
||
final String filePath;
|
||
final String? fileName;
|
||
|
||
UploadFileRequest({required this.filePath, this.fileName});
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => {}; // upload 不走 toJson
|
||
|
||
/// FormData — SDK 通过 uploadData 获取上传数据
|
||
@override
|
||
Object? get uploadData => FormData.fromMap({
|
||
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
|
||
});
|
||
}
|
||
</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<S3UploadResponse> {
|
||
final Uint8List data; // 二进制文件数据
|
||
final String presignedURL; // 后端返回的 S3 签名 URL
|
||
|
||
S3UploadRequest({required this.data, required this.presignedURL});
|
||
|
||
@override
|
||
String get path => presignedURL; // ← 完整 URL,SDK 检测到 http 开头不拼 baseURL
|
||
@override
|
||
HttpMethod get method => HttpMethod.put;
|
||
@override
|
||
ApiRequestType get requestType => ApiRequestType.upload;
|
||
@override
|
||
Map<String, String>? get customHeaders => {'Content-Type': 'application/octet-stream'};
|
||
@override
|
||
Map<String, dynamic> toJson() => {};
|
||
@override
|
||
Object? get uploadData => data; // Uint8List 直接作为 body
|
||
|
||
/// S3 返回 204 No Content 或 XML,不是标准 { code, msg, data } 信封
|
||
/// 必须 override decodeResponse
|
||
@override
|
||
S3UploadResponse? decodeResponse(Response response) {
|
||
if (response.statusCode != null &&
|
||
response.statusCode! >= 200 &&
|
||
response.statusCode! < 300) {
|
||
return const S3UploadResponse(success: true);
|
||
}
|
||
return const S3UploadResponse(success: false);
|
||
}
|
||
}
|
||
</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>Upload(FormData)</td><td><code>POST /upload/file</code></td><td><code>uploadData</code> → FormData</td><td>@ApiRequest + override uploadData</td></tr>
|
||
<tr><td>Upload(S3)</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<ApiConfig>((ref) {
|
||
return ApiConfig(
|
||
baseURL: AppConfig.apiBaseUrl,
|
||
platformHeaders: {
|
||
'Platform': 'Android', // TODO: 运行时从平台 API 获取
|
||
'client-version': '1.0.0', // TODO: 运行时从 package_info 获取
|
||
},
|
||
tokenExpiredCodes: {30002, 30003, 30124}, // 后端约定的 Token 过期错误码
|
||
forceLogoutCodes: {30125}, // 后端约定的强制登出错误码
|
||
onForceLogout: () { /* 清除登录态,跳转登录页 */ },
|
||
onTokenRefresh: () async { /* 调刷新 token 接口 */ return null; },
|
||
onLog: (message, {tag}) { print('[${tag ?? 'Network'}] $message'); },
|
||
);
|
||
});
|
||
|
||
/// API 客户端 Provider(全局单例)
|
||
/// 内部自动挂载 AuthInterceptor / RetryInterceptor / LoggingInterceptor
|
||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||
final config = ref.read(apiConfigProvider);
|
||
return ApiClient(config: config);
|
||
});
|
||
</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<AuthRepository>((ref) {
|
||
final apiConfig = ref.read(apiConfigProvider);
|
||
// final secureStorage = ref.read(secureStorageProvider); // storage_sdk(待接入)
|
||
|
||
return AuthRepositoryImpl(
|
||
client: ref.read(apiClientProvider), // 直接注入 ApiClient
|
||
onTokenUpdate: (token) {
|
||
apiConfig.updateToken(token); // 内存(networks_sdk)
|
||
// secureStorage.saveToken(token); // 持久化(storage_sdk,待接入)
|
||
},
|
||
);
|
||
});
|
||
</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: () => showToast('无网络连接'),
|
||
timeout: () => showToast('请求超时,请重试'),
|
||
networkError: (msg) => showToast('网络错误: $msg'),
|
||
decodingError: (msg) => showToast('数据解析失败'),
|
||
apiError: (code, msg) => showToast('服务端错误[$code]: $msg'),
|
||
unknown: (msg) => 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<String, dynamic> json) =>
|
||
_$LoginDataFromJson(json);
|
||
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
|
||
}
|
||
|
||
// ── Request ──
|
||
@ApiRequest(
|
||
path: ApiPaths.authLogin,
|
||
method: HttpMethod.post,
|
||
responseType: LoginData,
|
||
requestType: ApiRequestType.login,
|
||
)
|
||
@JsonSerializable()
|
||
class LoginRequest extends ApiRequestable<LoginData>
|
||
with _$LoginRequestApi { // ← 短暂报红,保存后自动消失
|
||
final String email;
|
||
final String password;
|
||
|
||
LoginRequest({required this.email, required this.password});
|
||
|
||
@override
|
||
Map<String, dynamic> toJson() => _$LoginRequestToJson(this); // ← 短暂报红,保存后自动消失
|
||
}
|
||
</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 SDK(packages/storage_sdk/)</h4>
|
||
|
||
<p>纯基础设施 SDK,不感知业务表结构。遵循 Facade + Wiring 模式,结构同 cipher_guard_sdk。</p>
|
||
|
||
<p><strong>职责边界:</strong></p>
|
||
<ul>
|
||
<li><strong>storage_sdk 负责</strong>:数据库连接生命周期(按 uid 隔离文件)、通用泛型 CRUD(insert / 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<StorageSdkApi>((ref) {
|
||
return StorageSdkApi(
|
||
databaseFactory: (executor) => AppDatabase(executor),
|
||
);
|
||
});
|
||
|
||
// 登录后开库
|
||
await ref.read(storageSdkProvider).openDatabase(userId);
|
||
|
||
// CRUD
|
||
final db = ref.read(storageSdkProvider);
|
||
await db.insertOrReplace(appDb.users, companion);
|
||
final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));
|
||
</code></pre>
|
||
|
||
<p><strong>公开接口(StorageSdkApi):</strong>生命周期(openDatabase / closeDatabase / isDatabaseOpen)+ 泛型 CRUD(insertOrReplace / batchInsertOrReplace / updateWhere / deleteWhere / deleteAll / selectAll / selectWhere / selectFirst / watchAll / watchWhere / watchFirst / rawQuery / rawExecute / count)。</p>
|
||
|
||
<p><strong>build.yaml(im_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 SDK(packages/media_sdk/)</h4>
|
||
|
||
<p>遵循 Facade + Wiring 模式。负责图片/视频处理,具体功能实现待开发。</p>
|
||
|
||
<h4>RTC SDK(packages/rtc_sdk/)</h4>
|
||
|
||
<p>遵循 Facade + Wiring 模式。负责实时音视频(WebRTC),具体功能实现待开发。</p>
|
||
|
||
<h4>Push SDK(packages/notification_sdk/)</h4>
|
||
|
||
<p>遵循 Facade + Wiring 模式。负责推送通知(FCM / APNs),具体功能实现待开发。</p>
|
||
|
||
<h4>Protocol SDK(packages/protocol_sdk/)</h4>
|
||
|
||
<p>遵循 Facade + Wiring 模式。负责消息协议(Protobuf 序列化),具体功能实现待开发。</p>
|
||
|
||
<h4>CipherGuard SDK(packages/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 Container),Dart 层和原生层均可读取</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 UI(core/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() => const ProfileState();
|
||
|
||
Future<void> loadProfile() async {
|
||
state = state.copyWith(isLoading: true, error: '');
|
||
|
||
try {
|
||
// 直接调 Repository,无需 UseCase 中间层
|
||
final profile = await ref.read(profileRepositoryProvider).getProfile();
|
||
state = state.copyWith(profile: profile);
|
||
} catch (e) {
|
||
state = state.copyWith(error: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
|
||
Future<void> updateProfile(Profile profile) async {
|
||
state = state.copyWith(isLoading: true);
|
||
|
||
try {
|
||
await ref.read(profileRepositoryProvider).updateProfile(profile);
|
||
state = state.copyWith(profile: profile);
|
||
} catch (e) {
|
||
state = state.copyWith(error: e.toString());
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<p><strong>步骤 4:在全局 Domain 层定义 Repository 接口</strong></p>
|
||
|
||
<pre><code>// domain/repositories/profile_repository.dart
|
||
abstract class ProfileRepository {
|
||
Future<Profile> getProfile();
|
||
Future<void> 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<Profile> getProfile() async {
|
||
final data = await _client.executeRequest(GetProfileRequest());
|
||
return data!.toEntity();
|
||
}
|
||
|
||
@override
|
||
Future<void> 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<ProfileRepository>((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: '>=3.0.0 <4.0.0'
|
||
|
||
dependencies:
|
||
flutter:
|
||
sdk: flutter
|
||
|
||
# 状态管理 - Riverpod
|
||
flutter_riverpod: ^2.4.0
|
||
riverpod_annotation: ^2.3.0
|
||
|
||
# 不可变状态 - Freezed
|
||
freezed_annotation: ^2.4.1
|
||
|
||
# JSON 序列化
|
||
json_annotation: ^4.8.1
|
||
|
||
# 依赖注入(可选,Riverpod 已包含依赖管理)
|
||
# get_it: ^7.6.0
|
||
# injectable: ^2.3.0
|
||
|
||
# 路由导航
|
||
go_router: ^12.0.0
|
||
|
||
# 网络请求
|
||
dio: ^5.4.0
|
||
web_socket_channel: ^2.4.0
|
||
|
||
# 本地存储
|
||
drift: ^2.15.0
|
||
sqlite3_flutter_libs: ^0.5.0
|
||
flutter_secure_storage: ^9.0.0
|
||
shared_preferences: ^2.2.0
|
||
|
||
# 加密
|
||
encrypt: ^5.0.3
|
||
crypto: ^3.0.3
|
||
|
||
# 媒体处理
|
||
image_picker: ^1.0.4
|
||
video_player: ^2.8.0
|
||
cached_network_image: ^3.3.0
|
||
|
||
# RTC
|
||
agora_rtc_engine: ^6.3.0
|
||
# 或者 flutter_webrtc: ^0.9.0
|
||
|
||
# 推送通知
|
||
firebase_messaging: ^14.7.0
|
||
flutter_local_notifications: ^16.3.0
|
||
|
||
# Protocol Buffers
|
||
protobuf: ^3.1.0
|
||
|
||
# 工具库
|
||
equatable: ^2.0.5
|
||
dartz: ^0.10.1
|
||
intl: ^0.18.1
|
||
|
||
dev_dependencies:
|
||
flutter_test:
|
||
sdk: flutter
|
||
|
||
# 代码生成
|
||
build_runner: ^2.4.6
|
||
riverpod_generator: ^2.3.0
|
||
freezed: ^2.4.5
|
||
json_serializable: ^6.7.1
|
||
drift_dev: ^2.15.0
|
||
|
||
# 代码检查
|
||
flutter_lints: ^3.0.0
|
||
very_good_analysis: ^5.1.0
|
||
|
||
# 测试
|
||
mocktail: ^1.0.1
|
||
integration_test:
|
||
sdk: flutter
|
||
</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 => const AppConfig(
|
||
isDebug: _kIsDebug,
|
||
apiBaseUrl: _kApiBaseUrl,
|
||
);
|
||
|
||
final bool isDebug;
|
||
final String apiBaseUrl;
|
||
|
||
bool get isProd => !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 # AAB(Google Play)
|
||
melos run build:ios
|
||
melos run build:macos
|
||
melos run build:windows
|
||
|
||
# prod 打包(需设置环境变量)
|
||
PROD_API_BASE_URL=https://api.example.com melos run build:android:apk -- apk prod
|
||
PROD_API_BASE_URL=https://api.example.com melos run build:android:aab -- aab prod
|
||
PROD_API_BASE_URL=https://api.example.com melos run build:ios -- prod
|
||
</code></pre>
|
||
|
||
<p><strong>体积优化说明</strong></p>
|
||
<ul>
|
||
<li><code>--split-debug-info</code>:将 Dart 调试符号从主产物中剥离,存入 <code>build/debug-info/<platform>/</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 Workflow(Gitea 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.lock(Dart 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 直接调 SDK(ApiClient / 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) => 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<ChatState> {
|
||
ChatViewModel(this._sendMessageUseCase) : super(const ChatState());
|
||
|
||
final SendMessageUseCase _sendMessageUseCase;
|
||
|
||
Future<void> sendMessage(String content) async {
|
||
state = state.copyWith(isLoading: true);
|
||
try {
|
||
await _sendMessageUseCase(content);
|
||
} finally {
|
||
state = state.copyWith(isLoading: false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Provider
|
||
final chatViewModelProvider =
|
||
StateNotifierProvider.autoDispose<ChatViewModel, ChatState>((ref) {
|
||
return ChatViewModel(ref.watch(sendMessageUseCaseProvider));
|
||
});
|
||
</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<Message> call(String content) {
|
||
return _repository.sendMessage(content);
|
||
}
|
||
}
|
||
|
||
// Provider
|
||
@riverpod
|
||
SendMessageUseCase sendMessageUseCase(SendMessageUseCaseRef ref) {
|
||
return SendMessageUseCase(ref.watch(messageRepositoryProvider));
|
||
}
|
||
|
||
// domain/repositories/message_repository.dart
|
||
abstract class MessageRepository {
|
||
Future<Message> sendMessage(String content);
|
||
Future<List<Message>> getMessages(String chatId);
|
||
}
|
||
</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<Message> sendMessage(String content) async {
|
||
// 1. 先保存到本地
|
||
final localMessage = await _localDS.saveMessage(content, status: 'sending');
|
||
|
||
// 2. 直接调 SDK 发送到服务器
|
||
try {
|
||
final serverMessage = await _client.executeRequest(
|
||
SendMessageRequest(chatId: localMessage.chatId, content: content),
|
||
);
|
||
|
||
// 3. 更新本地状态
|
||
await _localDS.updateMessageStatus(localMessage.id, 'sent');
|
||
|
||
return serverMessage!.toEntity();
|
||
} catch (e) {
|
||
await _localDS.updateMessageStatus(localMessage.id, 'failed');
|
||
rethrow;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Provider
|
||
@riverpod
|
||
MessageRepository messageRepository(MessageRepositoryRef ref) {
|
||
return MessageRepositoryImpl(
|
||
ref.watch(messageLocalDataSourceProvider),
|
||
ref.watch(apiClientProvider), // 直接注入 ApiClient
|
||
);
|
||
}
|
||
</code></pre>
|
||
|
||
<p><strong>5. Local DataSource + Provider</strong></p>
|
||
|
||
<pre><code>// data/local/message_local_ds.dart
|
||
class MessageLocalDataSource {
|
||
final AppDatabase _db;
|
||
|
||
Future<MessageDTO> saveMessage(String content, {required String status}) {
|
||
// Drift 操作
|
||
}
|
||
}
|
||
|
||
// Provider
|
||
@riverpod
|
||
MessageLocalDataSource messageLocalDataSource(MessageLocalDataSourceRef ref) {
|
||
return MessageLocalDataSourceImpl(ref.watch(databaseProvider));
|
||
}
|
||
</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<bool> isAvailable();
|
||
|
||
/// 初始化能力
|
||
Future<void> initialize();
|
||
|
||
/// 能力名称
|
||
String get name;
|
||
|
||
/// 能力版本
|
||
String get version;
|
||
}
|
||
|
||
/// Bridge 管理器 - 统一管理所有 Bridge 能力
|
||
class BridgeManager {
|
||
static final BridgeManager _instance = BridgeManager._internal();
|
||
factory BridgeManager() => _instance;
|
||
BridgeManager._internal();
|
||
|
||
final Map<String, BridgeCapability> _capabilities = {};
|
||
|
||
/// 注册能力
|
||
void registerCapability(BridgeCapability capability) {
|
||
_capabilities[capability.name] = capability;
|
||
}
|
||
|
||
/// 检测能力是否可用
|
||
Future<bool> hasCapability(String name) async {
|
||
final capability = _capabilities[name];
|
||
if (capability == null) return false;
|
||
return await capability.isAvailable();
|
||
}
|
||
|
||
/// 获取能力
|
||
T? getCapability<T extends BridgeCapability>(String name) {
|
||
return _capabilities[name] as T?;
|
||
}
|
||
}
|
||
|
||
/// 网络 Bridge 能力
|
||
class NetworkBridgeCapability implements BridgeCapability {
|
||
@override
|
||
String get name => 'network';
|
||
|
||
@override
|
||
String get version => '1.0.0';
|
||
|
||
@override
|
||
Future<bool> isAvailable() async {
|
||
// 检测宿主环境是否支持网络请求
|
||
return true;
|
||
}
|
||
|
||
@override
|
||
Future<void> initialize() async {
|
||
// 初始化网络能力
|
||
}
|
||
|
||
/// 通过 Bridge 发送网络请求
|
||
Future<Response> request(String url, {
|
||
required HTTPMethod method,
|
||
Map<String, dynamic>? data,
|
||
}) async {
|
||
// 调用宿主环境的网络能力
|
||
return await _callHost('network.request', {
|
||
'url': url,
|
||
'method': method.name,
|
||
'data': data,
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 存储 Bridge 能力
|
||
class StorageBridgeCapability implements BridgeCapability {
|
||
@override
|
||
String get name => 'storage';
|
||
|
||
@override
|
||
String get version => '1.0.0';
|
||
|
||
@override
|
||
Future<bool> isAvailable() async {
|
||
return true;
|
||
}
|
||
|
||
@override
|
||
Future<void> initialize() async {}
|
||
|
||
/// 存储数据
|
||
Future<void> setItem(String key, String value) async {
|
||
await _callHost('storage.set', {'key': key, 'value': value});
|
||
}
|
||
|
||
/// 读取数据
|
||
Future<String?> getItem(String key) async {
|
||
return await _callHost('storage.get', {'key': key});
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<h4>能力检测与降级策略</h4>
|
||
|
||
<pre><code class="language-dart">/// 能力检测与降级
|
||
class BridgeCapabilityChecker {
|
||
/// 检测所有必需能力
|
||
static Future<Map<String, bool>> checkRequiredCapabilities() async {
|
||
final manager = BridgeManager();
|
||
|
||
return {
|
||
'network': await manager.hasCapability('network'),
|
||
'storage': await manager.hasCapability('storage'),
|
||
'media': await manager.hasCapability('media'),
|
||
'push': await manager.hasCapability('push'),
|
||
'payment': await manager.hasCapability('payment'),
|
||
};
|
||
}
|
||
|
||
/// 根据能力启用功能
|
||
static Future<void> enableFeaturesBasedOnCapabilities() async {
|
||
final capabilities = await checkRequiredCapabilities();
|
||
|
||
// 网络能力不可用 - 降级到离线模式
|
||
if (!capabilities['network']!) {
|
||
FeatureToggle.enable('offline_mode');
|
||
}
|
||
|
||
// 推送能力不可用 - 使用轮询
|
||
if (!capabilities['push']!) {
|
||
FeatureToggle.enable('polling_mode');
|
||
}
|
||
|
||
// 支付能力不可用 - 隐藏支付功能
|
||
if (!capabilities['payment']!) {
|
||
FeatureToggle.disable('payment_feature');
|
||
}
|
||
}
|
||
}
|
||
</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>极快(<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<T> {
|
||
final MemoryCache memoryCache;
|
||
final HotTableDataSource hotTable;
|
||
final ColdTableDataSource coldTable;
|
||
final NetworkDataSource network;
|
||
|
||
const DataFetchStrategy({
|
||
required this.memoryCache,
|
||
required this.hotTable,
|
||
required this.coldTable,
|
||
required this.network,
|
||
});
|
||
|
||
/// 获取数据 - 自动多层查找
|
||
Future<T?> fetch(String key) async {
|
||
// L1: 检查内存缓存
|
||
final cached = memoryCache.get<T>(key);
|
||
if (cached != null) {
|
||
_recordHit('memory', key);
|
||
return cached;
|
||
}
|
||
|
||
// L2: 检查热表
|
||
final hot = await hotTable.query<T>(key);
|
||
if (hot != null) {
|
||
_recordHit('hot_table', key);
|
||
memoryCache.set(key, hot); // 回填内存
|
||
return hot;
|
||
}
|
||
|
||
// L3: 检查冷表
|
||
final cold = await coldTable.query<T>(key);
|
||
if (cold != null) {
|
||
_recordHit('cold_table', key);
|
||
// 提升到热表(如果访问频率高)
|
||
if (await _shouldPromoteToHot(key)) {
|
||
await hotTable.insert(key, cold);
|
||
}
|
||
memoryCache.set(key, cold); // 回填内存
|
||
return cold;
|
||
}
|
||
|
||
// L4: 网络请求
|
||
try {
|
||
final data = await network.fetch<T>(key);
|
||
if (data != null) {
|
||
_recordHit('network', key);
|
||
// 同步到各层缓存
|
||
await _syncToCache(key, data);
|
||
return data;
|
||
}
|
||
} catch (e) {
|
||
_recordError('network', key, e);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// 同步数据到缓存
|
||
Future<void> _syncToCache(String key, T data) async {
|
||
// 写入内存
|
||
memoryCache.set(key, data);
|
||
|
||
// 写入热表
|
||
await hotTable.insert(key, data);
|
||
|
||
// 写入冷表(持久化)
|
||
await coldTable.insert(key, data);
|
||
}
|
||
|
||
/// 判断是否应该提升到热表
|
||
Future<bool> _shouldPromoteToHot(String key) async {
|
||
final accessCount = await _getAccessCount(key);
|
||
return accessCount > 5; // 访问超过 5 次提升到热表
|
||
}
|
||
}
|
||
|
||
/// 内存缓存(LRU)
|
||
class MemoryCache {
|
||
final int maxSize;
|
||
final Map<String, dynamic> _cache = {};
|
||
final List<String> _accessOrder = [];
|
||
|
||
MemoryCache({this.maxSize = 1000});
|
||
|
||
T? get<T>(String key) {
|
||
if (!_cache.containsKey(key)) return null;
|
||
|
||
// 更新访问顺序
|
||
_accessOrder.remove(key);
|
||
_accessOrder.add(key);
|
||
|
||
return _cache[key] as T?;
|
||
}
|
||
|
||
void set(String key, dynamic value) {
|
||
// LRU 淘汰
|
||
if (_cache.length >= maxSize) {
|
||
final oldest = _accessOrder.removeAt(0);
|
||
_cache.remove(oldest);
|
||
}
|
||
|
||
_cache[key] = value;
|
||
_accessOrder.add(key);
|
||
}
|
||
}
|
||
|
||
/// 热表数据源(高频访问)
|
||
class HotTableDataSource {
|
||
/// 查询热表
|
||
Future<T?> query<T>(String key) async {
|
||
// SELECT * FROM hot_table WHERE key = ? AND last_access > (NOW() - 30 days)
|
||
return await _database.query('hot_table', where: 'key = ?', whereArgs: [key]);
|
||
}
|
||
|
||
/// 插入热表
|
||
Future<void> insert(String key, dynamic value) async {
|
||
await _database.insert('hot_table', {
|
||
'key': key,
|
||
'value': jsonEncode(value),
|
||
'last_access': DateTime.now().toIso8601String(),
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 冷表数据源(历史数据)
|
||
class ColdTableDataSource {
|
||
/// 查询冷表
|
||
Future<T?> query<T>(String key) async {
|
||
// SELECT * FROM cold_table WHERE key = ?
|
||
return await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
|
||
}
|
||
|
||
/// 插入冷表
|
||
Future<void> insert(String key, dynamic value) async {
|
||
await _database.insert('cold_table', {
|
||
'key': key,
|
||
'value': jsonEncode(value),
|
||
'created_at': DateTime.now().toIso8601String(),
|
||
});
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<h4>数据迁移策略</h4>
|
||
|
||
<pre><code class="language-dart">/// 数据迁移服务 - 热表与冷表之间的数据流动
|
||
class DataMigrationService {
|
||
/// 定期清理过期的热表数据
|
||
Future<void> cleanupHotTable() async {
|
||
// 删除 30 天未访问的数据
|
||
await _database.delete(
|
||
'hot_table',
|
||
where: 'last_access < ?',
|
||
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
|
||
);
|
||
}
|
||
|
||
/// 将热数据迁移到冷表
|
||
Future<void> migrateHotToCold() async {
|
||
final oldData = await _database.query(
|
||
'hot_table',
|
||
where: 'last_access < ?',
|
||
whereArgs: [DateTime.now().subtract(Duration(days: 30))],
|
||
);
|
||
|
||
for (final row in oldData) {
|
||
await _database.insert('cold_table', row);
|
||
}
|
||
}
|
||
|
||
/// 提升冷数据到热表
|
||
Future<void> promoteColdToHot(String key) async {
|
||
final data = await _database.query('cold_table', where: 'key = ?', whereArgs: [key]);
|
||
if (data != null) {
|
||
await _database.insert('hot_table', data);
|
||
}
|
||
}
|
||
}
|
||
</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<List<Message>> getMessages(String chatId);
|
||
Future<void> sendMessage(Message message);
|
||
}
|
||
|
||
// 2. Repository 实现层(直接注入 ApiClient)
|
||
class ChatRepositoryImpl implements ChatRepository {
|
||
final ApiClient client;
|
||
final LocalDataSource localDataSource;
|
||
final MessageMapper mapper;
|
||
|
||
ChatRepositoryImpl({
|
||
required this.client,
|
||
required this.localDataSource,
|
||
required this.mapper,
|
||
});
|
||
|
||
@override
|
||
Future<List<Message>> getMessages(String chatId) async {
|
||
// 先从本地获取
|
||
final localDTOs = await localDataSource.getMessages(chatId);
|
||
if (localDTOs.isNotEmpty) {
|
||
return localDTOs.map(mapper.toEntity).toList();
|
||
}
|
||
|
||
// 直接调 ApiClient 从远程获取
|
||
final response = await client.executeRequest(
|
||
GetMessagesRequest(chatId: chatId),
|
||
);
|
||
|
||
// 保存到本地
|
||
await localDataSource.saveMessages(response?.messages ?? []);
|
||
|
||
// 转换为 Entity
|
||
return (response?.messages ?? []).map(mapper.toEntity).toList();
|
||
}
|
||
}
|
||
|
||
// 4. 转换层(Mapper)
|
||
class MessageMapper {
|
||
Message toEntity(MessageDTO dto) {
|
||
return Message(
|
||
id: dto.id,
|
||
content: dto.content,
|
||
senderId: dto.senderId,
|
||
timestamp: DateTime.parse(dto.timestamp),
|
||
);
|
||
}
|
||
|
||
MessageDTO toDTO(Message entity) {
|
||
return MessageDTO(
|
||
id: entity.id,
|
||
content: entity.content,
|
||
senderId: entity.senderId,
|
||
timestamp: entity.timestamp.toIso8601String(),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 5. 策略层
|
||
abstract class CacheStrategy {
|
||
bool shouldCache(String key);
|
||
Duration getCacheDuration(String key);
|
||
}
|
||
|
||
class MessageCacheStrategy implements CacheStrategy {
|
||
@override
|
||
bool shouldCache(String key) {
|
||
// 最近 100 条消息缓存
|
||
return true;
|
||
}
|
||
|
||
@override
|
||
Duration getCacheDuration(String key) {
|
||
return Duration(hours: 24);
|
||
}
|
||
}
|
||
</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<void> smartCache() async {
|
||
// 预缓存最近联系人的头像
|
||
final recentContacts = await _getRecentContacts();
|
||
for (final contact in recentContacts) {
|
||
await ImageCache.precache(contact.avatar);
|
||
}
|
||
|
||
// 预缓存最近会话的最后几条消息
|
||
final recentChats = await _getRecentChats();
|
||
for (final chat in recentChats) {
|
||
await MessageCache.precache(chat.id, limit: 20);
|
||
}
|
||
}
|
||
|
||
/// 预加载 - 提前加载可能访问的数据
|
||
Future<void> preload() async {
|
||
// 预加载用户资料
|
||
await UserProfilePreloader.preload();
|
||
|
||
// 预加载表情包
|
||
await EmojiPreloader.preload();
|
||
|
||
// 预加载常用设置
|
||
await SettingsPreloader.preload();
|
||
}
|
||
|
||
/// 性能优化 - 优化关键路径
|
||
Future<void> optimize() async {
|
||
// 延迟加载非关键资源
|
||
await LazyLoader.load();
|
||
|
||
// 分帧渲染大列表
|
||
await ListOptimizer.optimize();
|
||
|
||
// 图片懒加载
|
||
await ImageLazyLoader.setup();
|
||
}
|
||
}
|
||
|
||
/// 响应时间监控
|
||
class ResponseTimeMonitor {
|
||
static void track(String operation, Duration duration) {
|
||
final ms = duration.inMilliseconds;
|
||
|
||
// 记录慢操作
|
||
if (ms > 100) {
|
||
Logger.warn('Slow operation: $operation took ${ms}ms');
|
||
}
|
||
|
||
// 上报性能数据
|
||
Analytics.track('response_time', {
|
||
'operation': operation,
|
||
'duration_ms': ms,
|
||
});
|
||
}
|
||
}
|
||
</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<String> checkArchitectureCompliance(String filePath) {
|
||
final issues = <String>[];
|
||
|
||
// 检查分层依赖
|
||
if (_hasReverseDependency(filePath)) {
|
||
issues.add('发现反向依赖:Domain 层不能依赖 Data 层');
|
||
}
|
||
|
||
// 检查跨层调用
|
||
if (_hasCrossLayerCall(filePath)) {
|
||
issues.add('发现跨层调用:UI 层不能直接调用 Repository');
|
||
}
|
||
|
||
return issues;
|
||
}
|
||
|
||
/// 代码质量检查
|
||
static List<String> checkCodeQuality(String filePath) {
|
||
final issues = <String>[];
|
||
|
||
// 检查类复杂度
|
||
if (_getClassComplexity(filePath) > 10) {
|
||
issues.add('类复杂度过高,建议拆分');
|
||
}
|
||
|
||
// 检查方法长度
|
||
if (_getMaxMethodLength(filePath) > 50) {
|
||
issues.add('方法过长,建议拆分');
|
||
}
|
||
|
||
// 检查重复代码
|
||
if (_hasDuplicateCode(filePath)) {
|
||
issues.add('发现重复代码,建议抽取');
|
||
}
|
||
|
||
return issues;
|
||
}
|
||
}
|
||
</code></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<String, dynamic>? data}) {
|
||
if (!kReleaseMode) {
|
||
_log(LogLevel.debug, message, data);
|
||
}
|
||
}
|
||
|
||
void info(String message, {Map<String, dynamic>? data}) {
|
||
_log(LogLevel.info, message, data);
|
||
_saveToLocal(LogLevel.info, message, data);
|
||
}
|
||
|
||
void warning(String message, {Map<String, dynamic>? data}) {
|
||
_log(LogLevel.warning, message, data);
|
||
_saveToLocal(LogLevel.warning, message, data);
|
||
_uploadToRemote(LogLevel.warning, message, data);
|
||
}
|
||
|
||
void error(String message, {
|
||
Map<String, dynamic>? data,
|
||
StackTrace? stackTrace,
|
||
}) {
|
||
_log(LogLevel.error, message, data);
|
||
_saveToLocal(LogLevel.error, message, data, stackTrace);
|
||
_uploadToRemote(LogLevel.error, message, data, stackTrace, immediate: true);
|
||
}
|
||
|
||
void fatal(String message, {
|
||
Map<String, dynamic>? data,
|
||
StackTrace? stackTrace,
|
||
}) {
|
||
_log(LogLevel.fatal, message, data);
|
||
_saveToLocal(LogLevel.fatal, message, data, stackTrace);
|
||
_uploadToRemote(LogLevel.fatal, message, data, stackTrace, immediate: true);
|
||
_triggerAlert(message, data, stackTrace);
|
||
}
|
||
|
||
/// 网络日志
|
||
void logNetworkRequest(String url, {
|
||
required HTTPMethod method,
|
||
Map<String, dynamic>? headers,
|
||
Map<String, dynamic>? params,
|
||
}) {
|
||
info('Network Request', data: {
|
||
'url': url,
|
||
'method': method.name,
|
||
'headers': _sanitizeHeaders(headers),
|
||
'params': _sanitizeData(params),
|
||
'timestamp': DateTime.now().toIso8601String(),
|
||
});
|
||
}
|
||
|
||
void logNetworkResponse(String url, {
|
||
required int statusCode,
|
||
required Duration duration,
|
||
String? error,
|
||
}) {
|
||
final level = statusCode >= 400 ? LogLevel.warning : LogLevel.info;
|
||
_log(level, 'Network Response', {
|
||
'url': url,
|
||
'statusCode': statusCode,
|
||
'duration': '${duration.inMilliseconds}ms',
|
||
'error': error,
|
||
});
|
||
}
|
||
|
||
/// 用户操作日志
|
||
void logUserAction(String action, {Map<String, dynamic>? data}) {
|
||
info('User Action: $action', data: data);
|
||
}
|
||
|
||
/// 性能日志
|
||
void logPerformance(String operation, Duration duration) {
|
||
if (duration.inMilliseconds > 100) {
|
||
warning('Slow Operation: $operation', data: {
|
||
'duration': '${duration.inMilliseconds}ms',
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 隐私数据脱敏
|
||
Map<String, dynamic>? _sanitizeData(Map<String, dynamic>? data) {
|
||
if (data == null) return null;
|
||
final sanitized = Map<String, dynamic>.from(data);
|
||
|
||
// 脱敏敏感字段
|
||
final sensitiveKeys = ['password', 'token', 'secret', 'phone', 'email'];
|
||
for (final key in sensitiveKeys) {
|
||
if (sanitized.containsKey(key)) {
|
||
sanitized[key] = '***';
|
||
}
|
||
}
|
||
|
||
return sanitized;
|
||
}
|
||
}
|
||
|
||
enum LogLevel { debug, info, warning, error, fatal }
|
||
</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>内存 > 500MB<br/>FPS < 50</td>
|
||
<td>实时</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>错误监控</strong></td>
|
||
<td>Crash 率、ANR 率、异常捕获</td>
|
||
<td>Crash 率 > 0.1%</td>
|
||
<td>实时</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>网络监控</strong></td>
|
||
<td>请求成功率、响应时间、流量消耗</td>
|
||
<td>成功率 < 95%<br/>响应时间 > 3s</td>
|
||
<td>每次请求</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>业务监控</strong></td>
|
||
<td>消息发送成功率、登录成功率</td>
|
||
<td>成功率 < 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 > 500 * 1024 * 1024) { // 500MB
|
||
LoggerService().warning('High Memory Usage', data: {
|
||
'memory': '${memoryUsage ~/ (1024 * 1024)}MB',
|
||
});
|
||
}
|
||
|
||
// 监控帧率
|
||
WidgetsBinding.instance.addTimingsCallback((timings) {
|
||
for (final timing in timings) {
|
||
final fps = 1000 / timing.totalSpan.inMilliseconds;
|
||
if (fps < 50) {
|
||
LoggerService().warning('Low FPS', data: {'fps': fps.toStringAsFixed(1)});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 错误监控 - Crash 捕获
|
||
void setupErrorMonitoring() {
|
||
// Flutter 错误捕获
|
||
FlutterError.onError = (details) {
|
||
LoggerService().fatal('Flutter Error', data: {
|
||
'exception': details.exception.toString(),
|
||
'stackTrace': details.stack.toString(),
|
||
});
|
||
};
|
||
|
||
// Dart 错误捕获
|
||
PlatformDispatcher.instance.onError = (error, stack) {
|
||
LoggerService().fatal('Dart Error', data: {
|
||
'error': error.toString(),
|
||
'stackTrace': stack.toString(),
|
||
});
|
||
return true;
|
||
};
|
||
}
|
||
|
||
/// 网络监控
|
||
void trackNetworkRequest({
|
||
required String url,
|
||
required DateTime startTime,
|
||
required DateTime endTime,
|
||
required int statusCode,
|
||
String? error,
|
||
}) {
|
||
final duration = endTime.difference(startTime);
|
||
|
||
// 记录请求
|
||
_recordMetric('network.request', {
|
||
'url': url,
|
||
'duration': duration.inMilliseconds,
|
||
'statusCode': statusCode,
|
||
'success': statusCode >= 200 && statusCode < 300,
|
||
'error': error,
|
||
});
|
||
|
||
// 慢请求告警
|
||
if (duration.inSeconds > 3) {
|
||
LoggerService().warning('Slow Network Request', data: {
|
||
'url': url,
|
||
'duration': '${duration.inSeconds}s',
|
||
});
|
||
}
|
||
|
||
// 请求失败告警
|
||
if (statusCode >= 400) {
|
||
LoggerService().error('Network Request Failed', data: {
|
||
'url': url,
|
||
'statusCode': statusCode,
|
||
'error': error,
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 业务监控
|
||
void trackBusinessEvent(String event, {
|
||
bool success = true,
|
||
Map<String, dynamic>? data,
|
||
}) {
|
||
_recordMetric('business.$event', {
|
||
'success': success,
|
||
...?data,
|
||
});
|
||
|
||
if (!success) {
|
||
LoggerService().warning('Business Event Failed: $event', data: data);
|
||
}
|
||
}
|
||
|
||
/// 用户行为监控
|
||
void trackUserBehavior(String page, String action, {Map<String, dynamic>? data}) {
|
||
_recordMetric('behavior', {
|
||
'page': page,
|
||
'action': action,
|
||
'timestamp': DateTime.now().toIso8601String(),
|
||
...?data,
|
||
});
|
||
}
|
||
|
||
/// 记录指标
|
||
void _recordMetric(String metric, Map<String, dynamic> data) {
|
||
// 本地缓存
|
||
_cacheMetric(metric, data);
|
||
|
||
// 批量上报(每 1 分钟或累计 100 条)
|
||
_scheduleUpload();
|
||
}
|
||
|
||
/// 批量上报
|
||
Future<void> _scheduleUpload() async {
|
||
// 实现批量上报逻辑
|
||
}
|
||
}
|
||
|
||
/// 使用示例
|
||
class ChatViewModel extends StateNotifier<ChatState> {
|
||
Future<void> sendMessage(String content) async {
|
||
final startTime = DateTime.now();
|
||
|
||
try {
|
||
await _sendMessageUseCase(content);
|
||
|
||
// 监控业务成功
|
||
MonitorService().trackBusinessEvent('send_message', success: true, data: {
|
||
'messageLength': content.length,
|
||
'duration': DateTime.now().difference(startTime).inMilliseconds,
|
||
});
|
||
|
||
// 监控用户行为
|
||
MonitorService().trackUserBehavior('chat', 'send_message');
|
||
|
||
} catch (e, stackTrace) {
|
||
// 监控业务失败
|
||
MonitorService().trackBusinessEvent('send_message', success: false, data: {
|
||
'error': e.toString(),
|
||
});
|
||
|
||
LoggerService().error('Send Message Failed', data: {
|
||
'error': e.toString(),
|
||
}, stackTrace: stackTrace);
|
||
|
||
rethrow;
|
||
}
|
||
}
|
||
}
|
||
</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>
|