Flutter: 项目设计 — ShopApp 架构规划
建楼先画蓝图——架构设计决定了代码的"承重墙"在哪。
📋 前置知识:需要先掌握以下内容
- 第1课:Flutter 简介与环境搭建
- 第2课:Dart 语言速览
- 第3课:Widget 基础
- 第4课:布局系统
- 第5课:StatefulWidget 与交互
- 第6课:Material Design 与常用组件
- 第7课:Phase 1 综合练习 — ShopApp 入门版
- 第8课:导航与路由
- 第9课:表单与输入
- 第10课:列表与滚动
- 第11课:网络请求与 REST API
- 第12课:状态管理 — Riverpod
- 第13课:本地存储
- 第14课:Phase 2 综合练习 — ShopApp 核心版
- 第15课:动画系统
- 第16课:主题与样式系统
- 第17课:Platform Channels 与原生交互
- 第18课:Firebase 集成
- 第19课:国际化与本地化
- 第20课:自定义绘制与 CustomPainter
- 第21课:测试 — 单元/Widget/集成测试
- 第22课:性能优化
- 第23课:Web 与 Desktop 多平台部署
- 第24课:CI/CD 自动化
- 第25课:多平台发布与商店审核
1. 你将学到
- 需求分析:用户故事(Alice 浏览下单 / Bob 商家管理 / Charlie 海淘支付)
- 技术选型:Flutter 3.x + Riverpod + GoRouter + Dio + Firebase + Hive
- 架构设计:Clean Architecture 分层(Domain / Data / Presentation)
- 数据建模:Product / Cart / Order / User 实体与 Firestore Schema
- UI/UX 设计:线框图 + 组件库规划 + 设计令牌体系
2. 一个没有架构的项目的故事
(1) 痛点:需求模糊 + 架构混乱
Bob 接到 ShopApp 项目时只有一句话需求:"做一个跨境电商应用"。他直接开始写代码,3 个月后发现:用户故事不明确(商家管理功能漏了)、数据模型频繁变更(3 次重写 Order)、架构耦合严重(改支付要动商品列表)。项目延期 2 个月。
(2) 先设计后编码的解法
项目设计阶段把需求、架构、数据模型、UI 规划全部想清楚,编码只是"照图施工"。
(3) 收益:编码时间减半 + 变更成本降 80%
Bob 用 2 周做设计后,编码 4 周完成(而非之前的 3+2 个月),需求变更只需改对应层,不影响其他模块。
3. 需求分析
(1) 用户故事
graph TD
subgraph User Stories
ALICE[Alice: Browse → Order → Pay $299.99]
BOB[Bob: Manage Products → View Orders]
CHARLIE[Charlie: Cross-border → JPY → International Payment]
end
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
| 角色 | 用户故事 | 优先级 |
|---|---|---|
| Alice (消费者) | 浏览千万级商品列表 | P0 |
| Alice | 搜索/筛选/分类浏览商品 | P0 |
| Alice | 查看商品详情 + 评价 | P0 |
| Alice | 加入购物车 + 管理数量 | P0 |
| Alice | USD/CNY/JPY 多币种结算 | P1 |
| Alice | 查看订单状态 + 物流追踪 | P1 |
| Bob (商家) | 管理商品上下架 + 库存 | P1 |
| Bob | 查看销售数据报表 | P2 |
| Charlie (跨境用户) | 海淘商品 + 跨境支付 | P2 |
| Charlie | 多语言切换(中/英/日) | P1 |
4. 技术选型
(1) 技术栈决策
| 领域 | 选型 | 理由 |
|---|---|---|
| UI 框架 | Flutter 3.x (Material 3) | 跨平台 + 自绘引擎 |
| 状态管理 | Riverpod 2.x (@riverpod) | 类型安全 + 代码生成 |
| 路由 | GoRouter | 声明式 + Web URL 支持 |
| 网络 | Dio + 拦截器 | 拦截器 + Token 刷新 |
| 后端 | Firebase (Auth+FS+Storage) | 零运维 + 实时同步 |
| 缓存 | Hive | 轻量 NoSQL + 离线 |
| 加密存储 | flutter_secure_storage | Token 安全 |
| 序列化 | json_serializable + freezed | 类型安全 + 不可变 |
| 国际化 | flutter_localizations + intl | ARB 工作流 |
| 测试 | flutter_test + mocktail | 单元/Widget/集成 |
| CI/CD | GitHub Actions | 免费额度 + 多矩阵 |
(2) Riverpod vs BLoC 对比
| 维度 | Riverpod | BLoC |
|---|---|---|
| 学习曲线 | 中 | 高 |
| 样板代码 | 少 (@riverpod) | 多 (Event/State/Bloc) |
| 类型安全 | 强 | 强 |
| 测试 | 简单 | 简单 |
| 代码生成 | ✅ | ❌ |
| 适用场景 | 中小型项目 | 大型团队项目 |
💡 提示: ShopApp 选 Riverpod,因为代码量少、代码生成减少样板、学习曲线适中。
5. Clean Architecture 分层
graph TD
subgraph Presentation
PAGE[Pages/Widgets]
NOTI[Notifiers/Providers]
end
subgraph Domain
ENT[Entities]
REPO_I[Repository Interfaces]
USECASE[Use Cases]
end
subgraph Data
REPO_IMPL[Repository Impl]
DS[Data Sources]
DTO2[DTOs / Models]
end
PAGE --> NOTI
NOTI --> USECASE
USECASE --> REPO_I
REPO_I -.-> REPO_IMPL
REPO_IMPL --> DS
DS --> DTO2
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(1) 分层职责
| 层 | 目录 | 职责 | 依赖方向 |
|---|---|---|---|
| Presentation | screens/, widgets/ |
UI + 用户交互 | → Application |
| Application | notifiers/, usecases/ |
业务逻辑 + 状态 | → Domain |
| Domain | entities/, repositories/ |
核心业务规则 | 无外部依赖 |
| Data | repositories/impl/, datasources/ |
数据获取 + 持久化 | → Domain |
(2) 依赖规则
- Domain 层:纯 Dart,无 Flutter/第三方依赖
- Data 层:实现 Domain 的 Repository 接口
- Application 层:协调 Domain 和 Presentation
- Presentation 层:只依赖 Application 层
6. 数据建模
(1) 核心实体
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:Domain Entity 定义
DART
// domain/entities/product.dart
// 纯 Dart 类,无外部依赖
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final String category;
final double rating;
final int reviewCount;
final int stock;
final String currency;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
this.category = 'General',
this.rating = 0.0,
this.reviewCount = 0,
this.stock = 0,
this.currency = 'USD',
});
bool get inStock => stock > 0;
bool get hasDiscount => false; // Extended in sale scenario
}
// domain/entities/cart_item.dart
class CartItem {
final Product product;
final int quantity;
final String? selectedSize;
final String? selectedColor;
const CartItem({
required this.product,
required this.quantity,
this.selectedSize,
this.selectedColor,
});
double get lineTotal => product.price * quantity;
CartItem copyWith({int? quantity, String? selectedSize, String? selectedColor}) =>
CartItem(product: product, quantity: quantity ?? this.quantity,
selectedSize: selectedSize ?? this.selectedSize,
selectedColor: selectedColor ?? this.selectedColor);
}
// domain/entities/address.dart
class Address {
final String name;
final String street;
final String city;
final String state;
final String zip;
final String country;
const Address({
required this.name,
required this.street,
required this.city,
required this.state,
required this.zip,
this.country = 'US',
});
String get fullAddress => '$street, $city, $state $zip, $country';
Map<String, dynamic> toJson() => {
'name': name, 'street': street, 'city': city,
'state': state, 'zip': zip, 'country': country,
};
}
// domain/entities/order.dart
enum OrderStatus { pending, confirmed, shipped, delivered, cancelled }
class Order {
final String id;
final String userId;
final List<CartItem> items;
final double total;
final String currency;
final OrderStatus status;
final DateTime createdAt;
final Address shippingAddress;
const Order({
required this.id,
required this.userId,
required this.items,
required this.total,
this.currency = 'USD',
this.status = OrderStatus.pending,
required this.createdAt,
required this.shippingAddress,
});
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(2) Firestore Schema 设计
TEXT
📖 仅展示
firestore/
├── products/{productId}
│ ├── name: string
│ ├── price: number
│ ├── stock: number
│ ├── category: string
│ ├── rating: number
│ ├── imageUrl: string
│ └── currency: string
├── users/{userId}
│ ├── email: string
│ ├── name: string
│ ├── preferences: map
│ │ ├── theme: string
│ │ ├── currency: string
│ │ └── locale: string
│ └── addresses: array
├── orders/{orderId}
│ ├── userId: string
│ ├── items: array<{productId, quantity, price}>
│ ├── total: number
│ ├── currency: string
│ ├── status: string
│ └── createdAt: timestamp
└── reviews/{reviewId}
├── productId: string
├── userId: string
├── rating: number
└── comment: string
```text
```text
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
7. UI/UX 规划
(1) 页面清单
| 页面 | 路由 | 核心组件 |
|---|---|---|
| 首页 | / |
SliverAppBar + GridView + CategoryChips |
| 搜索 | /search |
SearchBar + FilterSheet + ProductList |
| 商品详情 | /product/:id |
Hero + SliverAppBar + BottomSheet |
| 购物车 | /cart |
CartList + QuantitySelector + TotalBar |
| 结账 | /checkout |
AddressForm + PaymentSelector + OrderSummary |
| 订单确认 | /order/:id |
OrderTimeline + TrackingMap |
| 个人中心 | /profile |
UserCard + OrderHistory + Settings |
| 登录 | /login |
EmailForm + GoogleButton + AppleButton |
(2) 设计令牌
DART
import 'package:flutter/material.dart';
// 设计令牌:统一颜色/间距/圆角/字体常量,供 ThemeExtension 承载
class ShopDesignTokens {
// Colors
static const primary = Color(0xFF0066CC);
static const secondary = Color(0xFFFF6B35);
static const sale = Color(0xFF4CAF50);
static const discount = Color(0xFFEF5350);
// Spacing
static const xs = 4.0;
static const sm = 8.0;
static const md = 16.0;
static const lg = 24.0;
static const xl = 32.0;
// Radius
static const cardRadius = 12.0;
static const buttonRadius = 8.0;
static const inputRadius = 8.0;
// Typography
static const headlineSize = 24.0;
static const titleSize = 18.0;
static const bodySize = 14.0;
static const captionSize = 12.0;
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
8. 完整示例:项目目录结构
TEXT
📖 仅展示
shop_app/
├── lib/
│ ├── main.dart
│ ├── app.dart
│ ├── core/
│ │ ├── router/
│ │ │ └── app_router.dart
│ │ ├── network/
│ │ │ ├── dio_client.dart
│ │ │ └── api_exception.dart
│ │ ├── storage/
│ │ │ ├── secure_storage.dart
│ │ │ ├── hive_cache.dart
│ │ │ └── shared_prefs.dart
│ │ ├── theme/
│ │ │ ├── app_theme.dart
│ │ │ └── brand_tokens.dart
│ │ └── platform/
│ │ └── platform_helper.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── product.dart
│ │ │ ├── cart_item.dart
│ │ │ ├── order.dart
│ │ │ └── user.dart
│ │ └── repositories/
│ │ ├── product_repository.dart
│ │ ├── auth_repository.dart
│ │ └── order_repository.dart
│ ├── data/
│ │ ├── repositories/
│ │ │ ├── product_repository_impl.dart
│ │ │ ├── auth_repository_impl.dart
│ │ │ └── order_repository_impl.dart
│ │ ├── datasources/
│ │ │ ├── firestore_datasource.dart
│ │ │ └── hive_datasource.dart
│ │ └── models/
│ │ ├── product_dto.dart
│ │ └── order_dto.dart
│ ├── application/
│ │ ├── notifiers/
│ │ │ ├── auth_notifier.dart
│ │ │ ├── product_notifier.dart
│ │ │ └── cart_notifier.dart
│ │ └── providers/
│ │ └── infrastructure_providers.dart
│ ├── presentation/
│ │ ├── screens/
│ │ │ ├── home_page.dart
│ │ │ ├── detail_page.dart
│ │ │ ├── cart_page.dart
│ │ │ ├── checkout_page.dart
│ │ │ └── login_page.dart
│ │ └── widgets/
│ │ ├── product_card.dart
│ │ ├── quantity_selector.dart
│ │ ├── cart_badge.dart
│ │ └── price_tag.dart
│ └── l10n/
│ ├── app_en.arb
│ ├── app_zh.arb
│ └── app_ja.arb
├── test/
│ ├── domain/
│ ├── application/
│ └── presentation/
├── integration_test/
├── android/
├── ios/
├── web/
├── windows/
├── macos/
├── pubspec.yaml
├── l10n.yaml
└── .github/workflows/
```text
```text
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
❓ 常见问题
Q Clean Architecture 每层都需要吗?
A 中小型项目 Application 和 Domain 可以合并为 Application 层。Data 层始终需要(数据源抽象)。
Q Entity 和 DTO 有什么区别?
A Entity 是领域概念(业务逻辑用),DTO 是数据传输对象(API/数据库用)。Entity 不依赖外部,DTO 可能包含 JSON 序列化注解。
Q Repository 接口放 Domain 层有意义吗?
A 有。这是依赖反转原则:Domain 定义接口,Data 层实现。这样 Domain 层不依赖 Firebase/Dio 等外部库。
Q 项目设计阶段要花多久?
A 中小型项目 1-2 周。大型项目 2-4 周。设计投入 20% 时间,节省 50% 开发时间。
Q Firestore Schema 怎么设计最优?
A 1) 按 1:1 或 1:N 设计文档;2) 避免深层嵌套;3) 读频繁的数据反范式化(冗余存名称);4) 写频繁的数据范式化。
Q 怎么保证架构设计落地?
A 1) 用 lint 规则强制依赖方向;2) Code Review 检查分层合规;3) 单元测试覆盖每层边界。
📖 小节
- 用户故事驱动需求分析,按角色和优先级排列
- 技术选型需权衡团队能力、项目规模、生态成熟度
- Clean Architecture 四层:Presentation → Application → Domain → Data
- Domain 层纯 Dart 无外部依赖,Repository 接口定义在 Domain
- 设计令牌统一颜色/间距/圆角/字体,ThemeExtension 承载
📝 作业
- 基础题(难度⭐):为 ShopApp 编写 10 条用户故事,按 P0/P1/P2 优先级分类。
- 进阶题(难度⭐⭐):设计 Clean Architecture 四层目录结构,定义 Product/Cart/Order 三个 Entity 和对应 Repository 接口。
- 挑战题(难度⭐⭐⭐):完成完整的项目设计文档:用户故事 + 技术选型对比表 + Clean Architecture 分层图 + Firestore Schema + 页面清单 + 设计令牌。
(2) 示例:ShopApp 模块依赖图
DART
final Map<String, List<String>> moduleDeps = {
'core': [],
'cart': ['core'],
'checkout': ['core', 'cart'],
'profile': ['core'],
'catalog': ['core'],
};
输出:
TEXT
📖 仅展示
core: 0 直接依赖
cart: 依赖 core
checkout: 依赖 core, cart
profile: 依赖 core
catalog: 依赖 core
(3) 示例:路由表
DART
final Map<String, String> routes = {
'/': 'CatalogPage',
'/cart': 'CartPage',
'/checkout': 'CheckoutPage',
'/profile': 'ProfilePage',
};
输出:
TEXT
📖 仅展示
/ -> CatalogPage
/cart -> CartPage
/checkout -> CheckoutPage
/profile -> ProfilePage